GPS im Admin und offline verbesserungen
This commit is contained in:
@@ -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; }
|
||||
@@ -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 <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) {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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> 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);
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user