GPS im Admin und offline verbesserungen

This commit is contained in:
2026-05-24 10:43:57 +02:00
parent 91fb1fcf0b
commit 1899a49f4d
5 changed files with 1019 additions and 427 deletions
-357
View File
@@ -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; }
+538 -15
View File
@@ -12,17 +12,26 @@
<body class="page-admin">
<header>
<svg class="hdr-icon" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="11" width="18" height="10" rx="1" fill="#0088FF" opacity=".85"/>
<rect x="11" y="7" width="10" height="4" rx="1" fill="#0088FF" opacity=".5"/>
<circle cx="9" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="23" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="9" cy="23" r="1.3" fill="#0088FF"/>
<circle cx="23" cy="23" r="1.3" fill="#0088FF"/>
<rect x="22" y="8" width="4" height="3" rx=".5" fill="#FF6B00" opacity=".9"/>
<line x1="16" y1="7" x2="16" y2="3" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="16" cy="2.5" r="1" fill="#0088FF"/>
<rect x="3" y="14" width="4" height="2" rx="1" fill="#0088FF" opacity=".6"/>
<svg class="hdr-icon" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<ellipse cx="14" cy="51" rx="6" ry="7" fill="#FFB21F"/>
<ellipse cx="28" cy="50" rx="6" ry="8" fill="#FFB21F"/>
<ellipse cx="43" cy="50" rx="6" ry="8" fill="#FFB21F"/>
<ellipse cx="55" cy="49" rx="5" ry="7" fill="#FFB21F"/>
<path d="M11 36L23 30H50L58 32L47 37H14L11 36Z" fill="#1F43C9"/>
<rect x="24" y="14" width="7" height="23" rx="3.5" fill="#1B120F"/>
<path d="M18 38L25 36L41 36L51 40L51 46L42 49L38 59L31 59L29 50L19 47L18 38Z" fill="#1B120F"/>
<rect x="21" y="38" width="5" height="10" rx="2.5" fill="#1B120F"/>
<rect x="43" y="39" width="5" height="11" rx="2.5" fill="#1B120F"/>
<path d="M15 41L9 44L8 49" stroke="#1B120F" stroke-width="4" stroke-linecap="round"/>
<path d="M49 41L57 43L57 47" stroke="#1B120F" stroke-width="4" stroke-linecap="round"/>
<path d="M14 16C14 12 17 9 22 9H32C38 9 41 12 41 16C41 22 36 26 28 26H20C16 26 14 23 14 16Z" fill="#FFB21F"/>
<circle cx="21" cy="15.5" r="3.2" fill="white"/>
<circle cx="30.5" cy="14.5" r="3.6" fill="white"/>
<circle cx="21.5" cy="17" r="1.8" fill="#000"/>
<circle cx="30.8" cy="16" r="2.1" fill="#000"/>
<path d="M25 18C25.8 19.2 27.2 19.2 28 18V20.2C28 21.8 26.8 22.7 25.5 22.7C24.2 22.7 23 21.8 23 20.2V18C23.6 18.8 24.4 19.1 25 18Z" fill="#1B120F"/>
<path d="M12 12C13.5 9.2 17.8 8.2 21 10" stroke="#1B120F" stroke-width="2.8" stroke-linecap="round"/>
<path d="M29 7.5C31.8 6.2 35.6 7.1 37.6 9.7" stroke="#1B120F" stroke-width="2.8" stroke-linecap="round"/>
</svg>
<span class="hdr-title">CUNO&nbsp;<em>ADMIN</em></span>
<div class="hdr-div"></div>
@@ -30,6 +39,8 @@
<div class="hdr-gap"></div>
<span id="admin_status" class="hdr-status">Lade...</span>
<div class="hdr-div"></div>
<div class="mission-ok"><div class="pulse-dot"></div><span id="page-connection-label">Prüfe…</span></div>
<div class="hdr-div"></div>
<a class="hdr-back" href="./index.html">← Ground Control</a>
<div class="hdr-div"></div>
<div class="hdr-clk">
@@ -37,12 +48,17 @@
<div class="hdr-clk-val" id="utc-clock">--:--:--</div>
</div>
</header>
<div id="connection-banner" class="connection-banner" hidden>
<strong id="connection-banner-title">Verbindung wird geprüft</strong>
<span id="connection-banner-detail">Warte auf ROS und Admin-API.</span>
</div>
<div class="admin-shell">
<div class="admin-section-tabs" role="tablist" aria-label="Admin-Bereiche">
<button id="admin_tab_system" class="admin-section-tab is_active" role="tab" aria-selected="true" onclick="switchAdminSection('system')">Admin</button>
<button id="admin_tab_drive" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('drive')">Geschwindigkeit / Weg</button>
<button id="admin_tab_map" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('map')">Karte</button>
<button id="admin_tab_gps" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('gps')">GPS</button>
<button id="admin_tab_motor" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('motor')">Motorentest</button>
</div>
@@ -357,6 +373,64 @@
</div>
</section>
<section id="admin_panel_gps" class="admin-section-panel" role="tabpanel">
<div class="admin-section-grid admin-section-grid_gps">
<div class="admin-col admin-col_gps_main">
<div class="panel gps-panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">GNSS-Satelliten</span>
<span class="p-tag">Himmelskarte</span>
</div>
<div class="copy-text gps-copy">
Diese Ansicht zeigt die vom Empfänger gemeldeten Satelliten im Himmelsplot. Außen ist der Horizont, die Mitte ist der Zenit.
</div>
<div class="gps-sky-stage">
<canvas id="gps_sky_canvas" class="gps-sky-canvas" width="560" height="560"></canvas>
<div id="gps_satellite_tooltip" class="gps-satellite-tooltip" hidden></div>
</div>
</div>
</div>
<div class="admin-col admin-col_gps_side">
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Fix-Diagnose</span>
<span class="p-tag">GNSS-Diagnose</span>
</div>
<div class="sr"><span class="sr-led led-ok"></span><span class="sr-key">Status</span><span id="gps_diag_status" class="sr-val">Offline</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Fix-Qualität</span><span id="gps_diag_fix_quality" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Frame</span><span id="gps_diag_frame" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Breite</span><span id="gps_diag_lat" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Länge</span><span id="gps_diag_lon" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Höhe</span><span id="gps_diag_alt" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Geschwindigkeit</span><span id="gps_diag_speed" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Kurs</span><span id="gps_diag_track" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">UTC-Datum</span><span id="gps_diag_utc_date" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">UTC-Zeit</span><span id="gps_diag_utc_time" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Fix-Alter</span><span id="gps_diag_fix_age" class="sr-val">--</span></div>
</div>
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Genauigkeit</span>
<span class="p-tag">Qualität</span>
</div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Positionsgenauigkeit</span><span id="gps_diag_pdop" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Horizontale Genauigkeit</span><span id="gps_diag_hdop" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Vertikale Genauigkeit</span><span id="gps_diag_vdop" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Für Fix genutzt</span><span id="gps_diag_sat_used" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Im Himmelsplot</span><span id="gps_diag_sat_visible" class="sr-val">--</span></div>
<div class="copy-text gps-copy gps-copy_compact">
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.
</div>
</div>
</div>
</div>
</section>
<section id="admin_panel_motor" class="admin-section-panel motor-admin-tab" role="tabpanel">
<div class="admin-section-grid">
<div class="admin-col">
@@ -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 =
'<div class="gps-satellite-tooltip-title">' + (selected.prn || '--') + '</div>' +
'<div class="gps-satellite-tooltip-row"><span>System</span><strong>' + (selected.constellation || '--') + '</strong></div>' +
'<div class="gps-satellite-tooltip-row"><span>Richtung</span><strong>' + (typeof selected.azimuth === 'number' ? selected.azimuth + '°' : '--') + '</strong></div>' +
'<div class="gps-satellite-tooltip-row"><span>Höhe</span><strong>' + (typeof selected.elevation === 'number' ? selected.elevation + '°' : '--') + '</strong></div>' +
'<div class="gps-satellite-tooltip-row"><span>Signal</span><strong>' + (typeof selected.snr === 'number' ? selected.snr + ' dB' : '--') + '</strong></div>' +
'<div class="gps-satellite-tooltip-row"><span>Qualität</span><strong>' + (typeof selected.snr === 'number' ? (selected.snr >= 35 ? 'sehr gut' : (selected.snr >= 20 ? 'gut' : 'schwach')) : 'unbekannt') + '</strong></div>';
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) {
+149 -24
View File
@@ -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; }
+168 -25
View File
@@ -9,17 +9,26 @@
</head>
<body class="page-index">
<header>
<svg class="hdr-icon" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="11" width="18" height="10" rx="1" fill="#0088FF" opacity=".85"/>
<rect x="11" y="7" width="10" height="4" rx="1" fill="#0088FF" opacity=".5"/>
<circle cx="9" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="23" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="9" cy="23" r="1.3" fill="#0088FF"/>
<circle cx="23" cy="23" r="1.3" fill="#0088FF"/>
<rect x="22" y="8" width="4" height="3" rx=".5" fill="#FF6B00" opacity=".9"/>
<line x1="16" y1="7" x2="16" y2="3" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="16" cy="2.5" r="1" fill="#0088FF"/>
<rect x="3" y="14" width="4" height="2" rx="1" fill="#0088FF" opacity=".6"/>
<svg class="hdr-icon" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<ellipse cx="14" cy="51" rx="6" ry="7" fill="#FFB21F"/>
<ellipse cx="28" cy="50" rx="6" ry="8" fill="#FFB21F"/>
<ellipse cx="43" cy="50" rx="6" ry="8" fill="#FFB21F"/>
<ellipse cx="55" cy="49" rx="5" ry="7" fill="#FFB21F"/>
<path d="M11 36L23 30H50L58 32L47 37H14L11 36Z" fill="#1F43C9"/>
<rect x="24" y="14" width="7" height="23" rx="3.5" fill="#1B120F"/>
<path d="M18 38L25 36L41 36L51 40L51 46L42 49L38 59L31 59L29 50L19 47L18 38Z" fill="#1B120F"/>
<rect x="21" y="38" width="5" height="10" rx="2.5" fill="#1B120F"/>
<rect x="43" y="39" width="5" height="11" rx="2.5" fill="#1B120F"/>
<path d="M15 41L9 44L8 49" stroke="#1B120F" stroke-width="4" stroke-linecap="round"/>
<path d="M49 41L57 43L57 47" stroke="#1B120F" stroke-width="4" stroke-linecap="round"/>
<path d="M14 16C14 12 17 9 22 9H32C38 9 41 12 41 16C41 22 36 26 28 26H20C16 26 14 23 14 16Z" fill="#FFB21F"/>
<circle cx="21" cy="15.5" r="3.2" fill="white"/>
<circle cx="30.5" cy="14.5" r="3.6" fill="white"/>
<circle cx="21.5" cy="17" r="1.8" fill="#000"/>
<circle cx="30.8" cy="16" r="2.1" fill="#000"/>
<path d="M25 18C25.8 19.2 27.2 19.2 28 18V20.2C28 21.8 26.8 22.7 25.5 22.7C24.2 22.7 23 21.8 23 20.2V18C23.6 18.8 24.4 19.1 25 18Z" fill="#1B120F"/>
<path d="M12 12C13.5 9.2 17.8 8.2 21 10" stroke="#1B120F" stroke-width="2.8" stroke-linecap="round"/>
<path d="M29 7.5C31.8 6.2 35.6 7.1 37.6 9.7" stroke="#1B120F" stroke-width="2.8" stroke-linecap="round"/>
</svg>
<span class="hdr-title">EXO<em>MY</em>&nbsp;GROUND CTRL</span>
@@ -28,7 +37,7 @@
<div class="hdr-gap"></div>
<a class="admin-link" href="./admin.html">Admin</a>
<div class="hdr-div"></div>
<div class="mission-ok"><div class="pulse-dot"></div><span id="mission-state">Bereit</span></div>
<div class="mission-ok"><div class="pulse-dot"></div><span id="cuno-connection-status">Cuno offline</span></div>
<div class="hdr-div"></div>
<div class="hdr-clocks">
<div class="hdr-clk">
@@ -54,7 +63,7 @@
<div class="cam-port">
<div class="cam-frame" id="video_frame">
<img id="video_output" alt="Kamerabild vom ExoMy" />
<div class="c-rec"><div class="c-rec-dot"></div>LIVE</div>
<div class="c-rec" id="cam-live-indicator"><div class="c-rec-dot"></div><span id="cam-live-text">LIVE</span></div>
<div class="c-tl">
CAM: <span id="cam-state">Verbinde…</span><br>
MODE: <span id="overlay-mode">Ackermann</span>
@@ -74,16 +83,8 @@
STREAM: 8083
</div>
<div class="c-bm">
<div class="c-bm-box">
<span class="c-bm-lbl">V</span>
<strong id="overlay-speed">0,0</strong>
<span class="c-bm-unit">cm/s</span>
</div>
<div class="c-bm-box">
<span class="c-bm-lbl">S</span>
<strong id="overlay-distance">0,00</strong>
<span class="c-bm-unit">m</span>
</div>
V: <span id="overlay-speed">0,0</span> cm/s<br>
S: <span id="overlay-distance">0,00</span> m
</div>
<svg class="c-track" id="track-overlay" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
<path id="track-center" class="track-center" d=""></path>
@@ -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);
+164 -6
View File
@@ -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):