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">
|
<body class="page-admin">
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<svg class="hdr-icon" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="hdr-icon" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
<rect x="7" y="11" width="18" height="10" rx="1" fill="#0088FF" opacity=".85"/>
|
<ellipse cx="14" cy="51" rx="6" ry="7" fill="#FFB21F"/>
|
||||||
<rect x="11" y="7" width="10" height="4" rx="1" fill="#0088FF" opacity=".5"/>
|
<ellipse cx="28" cy="50" rx="6" ry="8" fill="#FFB21F"/>
|
||||||
<circle cx="9" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
|
<ellipse cx="43" cy="50" rx="6" ry="8" fill="#FFB21F"/>
|
||||||
<circle cx="23" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
|
<ellipse cx="55" cy="49" rx="5" ry="7" fill="#FFB21F"/>
|
||||||
<circle cx="9" cy="23" r="1.3" fill="#0088FF"/>
|
<path d="M11 36L23 30H50L58 32L47 37H14L11 36Z" fill="#1F43C9"/>
|
||||||
<circle cx="23" cy="23" r="1.3" fill="#0088FF"/>
|
<rect x="24" y="14" width="7" height="23" rx="3.5" fill="#1B120F"/>
|
||||||
<rect x="22" y="8" width="4" height="3" rx=".5" fill="#FF6B00" opacity=".9"/>
|
<path d="M18 38L25 36L41 36L51 40L51 46L42 49L38 59L31 59L29 50L19 47L18 38Z" fill="#1B120F"/>
|
||||||
<line x1="16" y1="7" x2="16" y2="3" stroke="#0088FF" stroke-width="1.5"/>
|
<rect x="21" y="38" width="5" height="10" rx="2.5" fill="#1B120F"/>
|
||||||
<circle cx="16" cy="2.5" r="1" fill="#0088FF"/>
|
<rect x="43" y="39" width="5" height="11" rx="2.5" fill="#1B120F"/>
|
||||||
<rect x="3" y="14" width="4" height="2" rx="1" fill="#0088FF" opacity=".6"/>
|
<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>
|
</svg>
|
||||||
<span class="hdr-title">CUNO <em>ADMIN</em></span>
|
<span class="hdr-title">CUNO <em>ADMIN</em></span>
|
||||||
<div class="hdr-div"></div>
|
<div class="hdr-div"></div>
|
||||||
@@ -30,6 +39,8 @@
|
|||||||
<div class="hdr-gap"></div>
|
<div class="hdr-gap"></div>
|
||||||
<span id="admin_status" class="hdr-status">Lade...</span>
|
<span id="admin_status" class="hdr-status">Lade...</span>
|
||||||
<div class="hdr-div"></div>
|
<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>
|
<a class="hdr-back" href="./index.html">← Ground Control</a>
|
||||||
<div class="hdr-div"></div>
|
<div class="hdr-div"></div>
|
||||||
<div class="hdr-clk">
|
<div class="hdr-clk">
|
||||||
@@ -37,12 +48,17 @@
|
|||||||
<div class="hdr-clk-val" id="utc-clock">--:--:--</div>
|
<div class="hdr-clk-val" id="utc-clock">--:--:--</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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-shell">
|
||||||
<div class="admin-section-tabs" role="tablist" aria-label="Admin-Bereiche">
|
<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_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_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_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>
|
<button id="admin_tab_motor" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('motor')">Motorentest</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -357,6 +373,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section id="admin_panel_motor" class="admin-section-panel motor-admin-tab" role="tabpanel">
|
||||||
<div class="admin-section-grid">
|
<div class="admin-section-grid">
|
||||||
<div class="admin-col">
|
<div class="admin-col">
|
||||||
@@ -479,6 +553,15 @@
|
|||||||
var motorCommandsTopic = null;
|
var motorCommandsTopic = null;
|
||||||
var gpsFixTopic = null;
|
var gpsFixTopic = null;
|
||||||
var gpsStatusTopic = 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 isMotorTestRunning = false;
|
||||||
var isContainerActionRunning = false;
|
var isContainerActionRunning = false;
|
||||||
@@ -560,6 +643,29 @@
|
|||||||
hasCenteredOnFix: false,
|
hasCenteredOnFix: false,
|
||||||
suppressMapClickUntil: 0
|
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() {
|
function submitPassword() {
|
||||||
var input = document.getElementById('auth_input');
|
var input = document.getElementById('auth_input');
|
||||||
@@ -578,6 +684,129 @@
|
|||||||
if (el) el.textContent = text;
|
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) {
|
function setMotorTestStatus(text) {
|
||||||
setAdminStatus(text);
|
setAdminStatus(text);
|
||||||
}
|
}
|
||||||
@@ -587,26 +816,32 @@
|
|||||||
var systemTab = document.getElementById('admin_tab_system');
|
var systemTab = document.getElementById('admin_tab_system');
|
||||||
var driveTab = document.getElementById('admin_tab_drive');
|
var driveTab = document.getElementById('admin_tab_drive');
|
||||||
var mapTab = document.getElementById('admin_tab_map');
|
var mapTab = document.getElementById('admin_tab_map');
|
||||||
|
var gpsTab = document.getElementById('admin_tab_gps');
|
||||||
var motorTab = document.getElementById('admin_tab_motor');
|
var motorTab = document.getElementById('admin_tab_motor');
|
||||||
var systemPanel = document.getElementById('admin_panel_system');
|
var systemPanel = document.getElementById('admin_panel_system');
|
||||||
var drivePanel = document.getElementById('admin_panel_drive');
|
var drivePanel = document.getElementById('admin_panel_drive');
|
||||||
var mapPanel = document.getElementById('admin_panel_map');
|
var mapPanel = document.getElementById('admin_panel_map');
|
||||||
|
var gpsPanel = document.getElementById('admin_panel_gps');
|
||||||
var motorPanel = document.getElementById('admin_panel_motor');
|
var motorPanel = document.getElementById('admin_panel_motor');
|
||||||
var isSystem = sectionName === 'system';
|
var isSystem = sectionName === 'system';
|
||||||
var isDrive = sectionName === 'drive';
|
var isDrive = sectionName === 'drive';
|
||||||
var isMap = sectionName === 'map';
|
var isMap = sectionName === 'map';
|
||||||
|
var isGps = sectionName === 'gps';
|
||||||
var isMotor = sectionName === 'motor';
|
var isMotor = sectionName === 'motor';
|
||||||
systemTab.classList.toggle('is_active', isSystem);
|
systemTab.classList.toggle('is_active', isSystem);
|
||||||
driveTab.classList.toggle('is_active', isDrive);
|
driveTab.classList.toggle('is_active', isDrive);
|
||||||
mapTab.classList.toggle('is_active', isMap);
|
mapTab.classList.toggle('is_active', isMap);
|
||||||
|
gpsTab.classList.toggle('is_active', isGps);
|
||||||
motorTab.classList.toggle('is_active', isMotor);
|
motorTab.classList.toggle('is_active', isMotor);
|
||||||
systemTab.setAttribute('aria-selected', isSystem ? 'true' : 'false');
|
systemTab.setAttribute('aria-selected', isSystem ? 'true' : 'false');
|
||||||
driveTab.setAttribute('aria-selected', isDrive ? 'true' : 'false');
|
driveTab.setAttribute('aria-selected', isDrive ? 'true' : 'false');
|
||||||
mapTab.setAttribute('aria-selected', isMap ? 'true' : 'false');
|
mapTab.setAttribute('aria-selected', isMap ? 'true' : 'false');
|
||||||
|
gpsTab.setAttribute('aria-selected', isGps ? 'true' : 'false');
|
||||||
motorTab.setAttribute('aria-selected', isMotor ? 'true' : 'false');
|
motorTab.setAttribute('aria-selected', isMotor ? 'true' : 'false');
|
||||||
systemPanel.classList.toggle('is_active', isSystem);
|
systemPanel.classList.toggle('is_active', isSystem);
|
||||||
drivePanel.classList.toggle('is_active', isDrive);
|
drivePanel.classList.toggle('is_active', isDrive);
|
||||||
mapPanel.classList.toggle('is_active', isMap);
|
mapPanel.classList.toggle('is_active', isMap);
|
||||||
|
gpsPanel.classList.toggle('is_active', isGps);
|
||||||
motorPanel.classList.toggle('is_active', isMotor);
|
motorPanel.classList.toggle('is_active', isMotor);
|
||||||
window.location.hash = isSystem ? '' : ('#' + sectionName);
|
window.location.hash = isSystem ? '' : ('#' + sectionName);
|
||||||
if (isMap) {
|
if (isMap) {
|
||||||
@@ -615,6 +850,9 @@
|
|||||||
if (roverMapState.map) roverMapState.map.invalidateSize();
|
if (roverMapState.map) roverMapState.map.invalidateSize();
|
||||||
}, 80);
|
}, 80);
|
||||||
}
|
}
|
||||||
|
if (isGps) {
|
||||||
|
window.setTimeout(drawGpsSkyPlot, 80);
|
||||||
|
}
|
||||||
if (isMotor) fetchMotorTestStatus();
|
if (isMotor) fetchMotorTestStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,14 +903,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCoordinate(value) {
|
function formatCoordinate(value, digits) {
|
||||||
if (typeof value !== 'number' || isNaN(value)) return '--';
|
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 '--';
|
if (typeof value !== 'number' || isNaN(value)) return '--';
|
||||||
return formatNumber(value, 1) + ' m';
|
return formatNumber(value, typeof digits === 'number' ? digits : 1) + ' m';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAccuracy(value) {
|
function formatAccuracy(value) {
|
||||||
@@ -680,6 +918,26 @@
|
|||||||
return formatNumber(value, 1) + ' m';
|
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) {
|
function formatFixTime(date) {
|
||||||
if (!date) return '--';
|
if (!date) return '--';
|
||||||
return date.toLocaleTimeString('de-DE', {
|
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() {
|
function updateMapTelemetryPanel() {
|
||||||
setText('map_gps_status', roverMapState.gpsStatusText || 'Offline');
|
setText('map_gps_status', roverMapState.gpsStatusText || 'Offline');
|
||||||
setText('map_gps_satellites', roverMapState.satellites);
|
setText('map_gps_satellites', roverMapState.satellites);
|
||||||
@@ -1113,6 +1606,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateGpsStatusSummary(text) {
|
function updateGpsStatusSummary(text) {
|
||||||
|
markAdminGpsFresh();
|
||||||
roverMapState.gpsStatusText = text || 'Offline';
|
roverMapState.gpsStatusText = text || 'Offline';
|
||||||
var satMatch = String(roverMapState.gpsStatusText).match(/(\d+)\s*Sat/i);
|
var satMatch = String(roverMapState.gpsStatusText).match(/(\d+)\s*Sat/i);
|
||||||
roverMapState.satellites = satMatch ? satMatch[1] : '--';
|
roverMapState.satellites = satMatch ? satMatch[1] : '--';
|
||||||
@@ -1121,6 +1615,7 @@
|
|||||||
|
|
||||||
function handleGpsFix(message) {
|
function handleGpsFix(message) {
|
||||||
if (!message || !message.status) return;
|
if (!message || !message.status) return;
|
||||||
|
markAdminGpsFresh();
|
||||||
|
|
||||||
roverMapState.hasFix = Number(message.status.status) >= 0;
|
roverMapState.hasFix = Number(message.status.status) >= 0;
|
||||||
if (!roverMapState.hasFix) {
|
if (!roverMapState.hasFix) {
|
||||||
@@ -1450,6 +1945,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDriveEstimator() {
|
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();
|
updateDriveEstimatorModel();
|
||||||
setText('drive_estimator_speed', formatSpeedCms(driveEstimator.speedMps, 1));
|
setText('drive_estimator_speed', formatSpeedCms(driveEstimator.speedMps, 1));
|
||||||
setText('drive_estimator_distance', formatNumber(driveEstimator.distanceM, 2));
|
setText('drive_estimator_distance', formatNumber(driveEstimator.distanceM, 2));
|
||||||
@@ -1572,9 +2076,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
ros.on('connection', function() {
|
ros.on('connection', function() {
|
||||||
|
markAdminRosConnection(true);
|
||||||
setDriveEstimatorState('verbunden', 'state_active');
|
setDriveEstimatorState('verbunden', 'state_active');
|
||||||
});
|
});
|
||||||
ros.on('close', function() {
|
ros.on('close', function() {
|
||||||
|
markAdminRosConnection(false);
|
||||||
setDriveEstimatorState('getrennt', 'state_inactive');
|
setDriveEstimatorState('getrennt', 'state_inactive');
|
||||||
ros = null;
|
ros = null;
|
||||||
roverCommandTopic = null;
|
roverCommandTopic = null;
|
||||||
@@ -1582,6 +2088,7 @@
|
|||||||
window.setTimeout(connectDriveEstimatorRos, 3000);
|
window.setTimeout(connectDriveEstimatorRos, 3000);
|
||||||
});
|
});
|
||||||
ros.on('error', function() {
|
ros.on('error', function() {
|
||||||
|
markAdminRosConnection(false);
|
||||||
setDriveEstimatorState('Fehler', 'state_unknown');
|
setDriveEstimatorState('Fehler', 'state_unknown');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1621,6 +2128,13 @@
|
|||||||
gpsStatusTopic.subscribe(function(message) {
|
gpsStatusTopic.subscribe(function(message) {
|
||||||
updateGpsStatusSummary(message.data);
|
updateGpsStatusSummary(message.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gpsDiagnosticsTopic = new ROSLIB.Topic({
|
||||||
|
ros: ros,
|
||||||
|
name: '/gps/diagnostics_json',
|
||||||
|
messageType: 'std_msgs/String'
|
||||||
|
});
|
||||||
|
gpsDiagnosticsTopic.subscribe(handleGpsDiagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
function temperatureToWidth(value) {
|
function temperatureToWidth(value) {
|
||||||
@@ -1649,8 +2163,10 @@
|
|||||||
try {
|
try {
|
||||||
var response = await fetch(adminApiBase + '/api/status');
|
var response = await fetch(adminApiBase + '/api/status');
|
||||||
if (!response.ok) throw new Error('status ' + response.status);
|
if (!response.ok) throw new Error('status ' + response.status);
|
||||||
|
markAdminApiConnection(true);
|
||||||
renderStatus(await response.json());
|
renderStatus(await response.json());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
markAdminApiConnection(false);
|
||||||
setAdminStatus('API nicht erreichbar');
|
setAdminStatus('API nicht erreichbar');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2116,18 +2632,23 @@
|
|||||||
|
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
var input = document.getElementById('auth_input');
|
var input = document.getElementById('auth_input');
|
||||||
|
var gpsCanvas = document.getElementById('gps_sky_canvas');
|
||||||
input.focus();
|
input.focus();
|
||||||
input.addEventListener('keydown', function(e) {
|
input.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Enter') submitPassword();
|
if (e.key === 'Enter') submitPassword();
|
||||||
});
|
});
|
||||||
|
if (gpsCanvas) gpsCanvas.addEventListener('click', onGpsSkyCanvasClick);
|
||||||
if (window.location.hash === '#motor') {
|
if (window.location.hash === '#motor') {
|
||||||
switchAdminSection('motor');
|
switchAdminSection('motor');
|
||||||
} else if (window.location.hash === '#drive') {
|
} else if (window.location.hash === '#drive') {
|
||||||
switchAdminSection('drive');
|
switchAdminSection('drive');
|
||||||
|
} else if (window.location.hash === '#gps') {
|
||||||
|
switchAdminSection('gps');
|
||||||
} else if (window.location.hash === '#map') {
|
} else if (window.location.hash === '#map') {
|
||||||
switchAdminSection('map');
|
switchAdminSection('map');
|
||||||
}
|
}
|
||||||
updateClock();
|
updateClock();
|
||||||
|
updatePageConnectionState();
|
||||||
window.setInterval(updateClock, 1000);
|
window.setInterval(updateClock, 1000);
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
fetchDelay();
|
fetchDelay();
|
||||||
@@ -2140,11 +2661,13 @@
|
|||||||
renderWaypointList();
|
renderWaypointList();
|
||||||
applyWaypointPlanningState();
|
applyWaypointPlanningState();
|
||||||
renderDriveEstimator();
|
renderDriveEstimator();
|
||||||
|
updateGpsDiagnosticsPanel();
|
||||||
window.setInterval(fetchStatus, 10000);
|
window.setInterval(fetchStatus, 10000);
|
||||||
window.setInterval(fetchDelay, 5000);
|
window.setInterval(fetchDelay, 5000);
|
||||||
window.setInterval(fetchSpeedLimit, 5000);
|
window.setInterval(fetchSpeedLimit, 5000);
|
||||||
window.setInterval(fetchMotorTestStatus, 4000);
|
window.setInterval(fetchMotorTestStatus, 4000);
|
||||||
window.setInterval(renderDriveEstimator, 250);
|
window.setInterval(renderDriveEstimator, 250);
|
||||||
|
window.setInterval(updatePageConnectionState, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('beforeunload', function(event) {
|
window.addEventListener('beforeunload', function(event) {
|
||||||
|
|||||||
@@ -184,6 +184,15 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#stat-panel { flex-shrink: 0; }
|
#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; }
|
.drive-btns { display: flex; gap: 8px; padding: 10px 12px; }
|
||||||
.dbtn {
|
.dbtn {
|
||||||
@@ -298,6 +307,9 @@ header {
|
|||||||
letter-spacing: calc(2px * var(--cam-scale));
|
letter-spacing: calc(2px * var(--cam-scale));
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
.c-rec.is-offline {
|
||||||
|
color: rgba(170,255,220,.72);
|
||||||
|
}
|
||||||
.c-rec-dot {
|
.c-rec-dot {
|
||||||
width: calc(6px * var(--cam-scale));
|
width: calc(6px * var(--cam-scale));
|
||||||
height: calc(6px * var(--cam-scale));
|
height: calc(6px * var(--cam-scale));
|
||||||
@@ -305,6 +317,10 @@ header {
|
|||||||
background: #FF3333;
|
background: #FF3333;
|
||||||
animation: blink 1.1s step-end infinite;
|
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; } }
|
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0; } }
|
||||||
.c-tl, .c-tr, .c-bl, .c-br {
|
.c-tl, .c-tr, .c-bl, .c-br {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -324,34 +340,59 @@ header {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: calc(13px * var(--cam-scale));
|
bottom: calc(13px * var(--cam-scale));
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: calc(8px * var(--cam-scale));
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
pointer-events: none;
|
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);
|
color: rgba(0,255,145,.72);
|
||||||
letter-spacing: calc(1.2px * var(--cam-scale));
|
font-size: calc(9px * 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);
|
|
||||||
letter-spacing: calc(1px * 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 ─────────────────────────────────────── */
|
/* ── STAT PANEL ─────────────────────────────────────── */
|
||||||
.c-track {
|
.c-track {
|
||||||
@@ -1186,13 +1227,23 @@ body.page-admin .drive-calibration-button{
|
|||||||
body.page-admin .admin-section-grid_map{
|
body.page-admin .admin-section-grid_map{
|
||||||
grid-template-columns:minmax(0, 1.7fr) minmax(280px, .85fr);
|
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_main,
|
||||||
body.page-admin .admin-col_map_side{ min-width:0; }
|
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{
|
body.page-admin .map-panel{
|
||||||
display:flex;
|
display:flex;
|
||||||
flex-direction:column;
|
flex-direction:column;
|
||||||
min-height:620px;
|
min-height:620px;
|
||||||
}
|
}
|
||||||
|
body.page-admin .gps-panel{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
min-height:620px;
|
||||||
|
}
|
||||||
body.page-admin .map-toolbar{
|
body.page-admin .map-toolbar{
|
||||||
display:flex;
|
display:flex;
|
||||||
gap:8px;
|
gap:8px;
|
||||||
@@ -1230,6 +1281,78 @@ body.page-admin .map-route-storage-input{
|
|||||||
body.page-admin .map-route-storage-input:focus{
|
body.page-admin .map-route-storage-input:focus{
|
||||||
border-color:rgba(0,255,145,.5);
|
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{
|
body.page-admin .map-stage{
|
||||||
position:relative;
|
position:relative;
|
||||||
flex:1;
|
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-toolbar{ flex-direction:column; align-items:stretch; }
|
||||||
body.page-admin .drive-calibration-grid{ grid-template-columns:1fr; }
|
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_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 .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-stage{ min-height:360px; }
|
||||||
body.page-admin .map-route-storage,
|
body.page-admin .map-route-storage,
|
||||||
body.page-admin .map-route-storage_secondary{ grid-template-columns:1fr; }
|
body.page-admin .map-route-storage_secondary{ grid-template-columns:1fr; }
|
||||||
|
|||||||
@@ -9,17 +9,26 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="page-index">
|
<body class="page-index">
|
||||||
<header>
|
<header>
|
||||||
<svg class="hdr-icon" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="hdr-icon" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
<rect x="7" y="11" width="18" height="10" rx="1" fill="#0088FF" opacity=".85"/>
|
<ellipse cx="14" cy="51" rx="6" ry="7" fill="#FFB21F"/>
|
||||||
<rect x="11" y="7" width="10" height="4" rx="1" fill="#0088FF" opacity=".5"/>
|
<ellipse cx="28" cy="50" rx="6" ry="8" fill="#FFB21F"/>
|
||||||
<circle cx="9" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
|
<ellipse cx="43" cy="50" rx="6" ry="8" fill="#FFB21F"/>
|
||||||
<circle cx="23" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
|
<ellipse cx="55" cy="49" rx="5" ry="7" fill="#FFB21F"/>
|
||||||
<circle cx="9" cy="23" r="1.3" fill="#0088FF"/>
|
<path d="M11 36L23 30H50L58 32L47 37H14L11 36Z" fill="#1F43C9"/>
|
||||||
<circle cx="23" cy="23" r="1.3" fill="#0088FF"/>
|
<rect x="24" y="14" width="7" height="23" rx="3.5" fill="#1B120F"/>
|
||||||
<rect x="22" y="8" width="4" height="3" rx=".5" fill="#FF6B00" opacity=".9"/>
|
<path d="M18 38L25 36L41 36L51 40L51 46L42 49L38 59L31 59L29 50L19 47L18 38Z" fill="#1B120F"/>
|
||||||
<line x1="16" y1="7" x2="16" y2="3" stroke="#0088FF" stroke-width="1.5"/>
|
<rect x="21" y="38" width="5" height="10" rx="2.5" fill="#1B120F"/>
|
||||||
<circle cx="16" cy="2.5" r="1" fill="#0088FF"/>
|
<rect x="43" y="39" width="5" height="11" rx="2.5" fill="#1B120F"/>
|
||||||
<rect x="3" y="14" width="4" height="2" rx="1" fill="#0088FF" opacity=".6"/>
|
<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>
|
</svg>
|
||||||
|
|
||||||
<span class="hdr-title">EXO<em>MY</em> GROUND CTRL</span>
|
<span class="hdr-title">EXO<em>MY</em> GROUND CTRL</span>
|
||||||
@@ -28,7 +37,7 @@
|
|||||||
<div class="hdr-gap"></div>
|
<div class="hdr-gap"></div>
|
||||||
<a class="admin-link" href="./admin.html">Admin</a>
|
<a class="admin-link" href="./admin.html">Admin</a>
|
||||||
<div class="hdr-div"></div>
|
<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-div"></div>
|
||||||
<div class="hdr-clocks">
|
<div class="hdr-clocks">
|
||||||
<div class="hdr-clk">
|
<div class="hdr-clk">
|
||||||
@@ -54,7 +63,7 @@
|
|||||||
<div class="cam-port">
|
<div class="cam-port">
|
||||||
<div class="cam-frame" id="video_frame">
|
<div class="cam-frame" id="video_frame">
|
||||||
<img id="video_output" alt="Kamerabild vom ExoMy" />
|
<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">
|
<div class="c-tl">
|
||||||
CAM: <span id="cam-state">Verbinde…</span><br>
|
CAM: <span id="cam-state">Verbinde…</span><br>
|
||||||
MODE: <span id="overlay-mode">Ackermann</span>
|
MODE: <span id="overlay-mode">Ackermann</span>
|
||||||
@@ -74,16 +83,8 @@
|
|||||||
STREAM: 8083
|
STREAM: 8083
|
||||||
</div>
|
</div>
|
||||||
<div class="c-bm">
|
<div class="c-bm">
|
||||||
<div class="c-bm-box">
|
V: <span id="overlay-speed">0,0</span> cm/s<br>
|
||||||
<span class="c-bm-lbl">V</span>
|
S: <span id="overlay-distance">0,00</span> m
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<svg class="c-track" id="track-overlay" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
<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>
|
<path id="track-center" class="track-center" d=""></path>
|
||||||
@@ -310,6 +311,13 @@ var trajectoryOverlay = {
|
|||||||
steering: 90,
|
steering: 90,
|
||||||
velocity: 0
|
velocity: 0
|
||||||
};
|
};
|
||||||
|
var pageConnection = {
|
||||||
|
apiOk: false,
|
||||||
|
apiLastAt: 0,
|
||||||
|
rosOk: false,
|
||||||
|
gpsLastAt: 0,
|
||||||
|
state: "offline"
|
||||||
|
};
|
||||||
|
|
||||||
function setText(id, text) {
|
function setText(id, text) {
|
||||||
var el = document.getElementById(id);
|
var el = document.getElementById(id);
|
||||||
@@ -445,6 +453,11 @@ function updateDriveEstimatorModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDriveEstimatorOverlay() {
|
function renderDriveEstimatorOverlay() {
|
||||||
|
if (!pageConnection.rosOk) {
|
||||||
|
setText("overlay-speed", "--");
|
||||||
|
setText("overlay-distance", "--");
|
||||||
|
return;
|
||||||
|
}
|
||||||
updateDriveEstimatorModel();
|
updateDriveEstimatorModel();
|
||||||
setText("overlay-speed", formatSpeedCms(driveEstimator.speedMps, 1));
|
setText("overlay-speed", formatSpeedCms(driveEstimator.speedMps, 1));
|
||||||
setText("overlay-distance", formatNumber(driveEstimator.distanceM, 2));
|
setText("overlay-distance", formatNumber(driveEstimator.distanceM, 2));
|
||||||
@@ -592,6 +605,7 @@ function updateAxesDisplay(x, y) {
|
|||||||
|
|
||||||
function updateGpsOverlayFromFix(message) {
|
function updateGpsOverlayFromFix(message) {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
|
markGpsFresh();
|
||||||
|
|
||||||
var hasFix = message.status && message.status.status >= 0;
|
var hasFix = message.status && message.status.status >= 0;
|
||||||
if (!hasFix) {
|
if (!hasFix) {
|
||||||
@@ -606,6 +620,7 @@ function updateGpsOverlayFromFix(message) {
|
|||||||
|
|
||||||
function updateGpsOverlayFromStatus(text) {
|
function updateGpsOverlayFromStatus(text) {
|
||||||
var value = String(text || "Offline");
|
var value = String(text || "Offline");
|
||||||
|
markGpsFresh();
|
||||||
setText("gps-state", value);
|
setText("gps-state", value);
|
||||||
|
|
||||||
var satMatch = value.match(/(\d+)\s*Sat/i);
|
var satMatch = value.match(/(\d+)\s*Sat/i);
|
||||||
@@ -675,7 +690,96 @@ function updateInputDisplay() {
|
|||||||
function setRosStatus(text) {
|
function setRosStatus(text) {
|
||||||
setText("status", text);
|
setText("status", text);
|
||||||
setText("ros-state", 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) {
|
function updateUndervoltageDisplay(text, state) {
|
||||||
@@ -727,14 +831,16 @@ function fetchSystemStatus() {
|
|||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
}).then(function (payload) {
|
}).then(function (payload) {
|
||||||
|
markApiConnection(true);
|
||||||
updateServiceState(payload);
|
updateServiceState(payload);
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
|
markApiConnection(false);
|
||||||
setText("stat-wifi", "nicht erreichbar");
|
setText("stat-wifi", "nicht erreichbar");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function publishJoy() {
|
function publishJoy() {
|
||||||
if (!joyListener) {
|
if (!joyListener || controlsAreBlocked()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
joyListener.publish(new ROSLIB.Message({
|
joyListener.publish(new ROSLIB.Message({
|
||||||
@@ -772,6 +878,9 @@ function resetAxes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pulseButton(buttonIndex) {
|
function pulseButton(buttonIndex) {
|
||||||
|
if (controlsAreBlocked()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
axes = [0, 0, 0, 0, 0, 0];
|
axes = [0, 0, 0, 0, 0, 0];
|
||||||
updateAxesDisplay(0, 0);
|
updateAxesDisplay(0, 0);
|
||||||
buttons[buttonIndex] = 1;
|
buttons[buttonIndex] = 1;
|
||||||
@@ -783,11 +892,17 @@ function pulseButton(buttonIndex) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectDriveMode(mode, buttonIndex) {
|
function selectDriveMode(mode, buttonIndex) {
|
||||||
|
if (controlsAreBlocked()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
applyDriveMode(mode);
|
applyDriveMode(mode);
|
||||||
pulseButton(buttonIndex);
|
pulseButton(buttonIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMotors() {
|
function toggleMotors() {
|
||||||
|
if (controlsAreBlocked()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
motorsEnabled = !motorsEnabled;
|
motorsEnabled = !motorsEnabled;
|
||||||
updateMotorDisplay();
|
updateMotorDisplay();
|
||||||
pulseButton(9);
|
pulseButton(9);
|
||||||
@@ -831,6 +946,9 @@ function joyMetrics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function moveThumb(clientX, clientY) {
|
function moveThumb(clientX, clientY) {
|
||||||
|
if (controlsAreBlocked()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var data = joyMetrics();
|
var data = joyMetrics();
|
||||||
var dx = clientX - data.cx;
|
var dx = clientX - data.cx;
|
||||||
var dy = clientY - data.cy;
|
var dy = clientY - data.cy;
|
||||||
@@ -865,6 +983,9 @@ function resetThumb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setDpadPressed(button, x, y) {
|
function setDpadPressed(button, x, y) {
|
||||||
|
if (controlsAreBlocked()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
|
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
|
||||||
btn.classList.remove("pressed");
|
btn.classList.remove("pressed");
|
||||||
});
|
});
|
||||||
@@ -959,10 +1080,12 @@ window.addEventListener("load", function () {
|
|||||||
updateClocks();
|
updateClocks();
|
||||||
window.setInterval(updateClocks, 1000);
|
window.setInterval(updateClocks, 1000);
|
||||||
fetchSystemStatus();
|
fetchSystemStatus();
|
||||||
|
setPageConnectionState();
|
||||||
fetchDriveEstimatorData();
|
fetchDriveEstimatorData();
|
||||||
renderDriveEstimatorOverlay();
|
renderDriveEstimatorOverlay();
|
||||||
renderTrajectoryOverlay();
|
renderTrajectoryOverlay();
|
||||||
statusTimer = window.setInterval(fetchSystemStatus, 5000);
|
statusTimer = window.setInterval(fetchSystemStatus, 5000);
|
||||||
|
window.setInterval(setPageConnectionState, 1000);
|
||||||
window.setInterval(renderDriveEstimatorOverlay, 250);
|
window.setInterval(renderDriveEstimatorOverlay, 250);
|
||||||
window.setInterval(renderTrajectoryOverlay, 250);
|
window.setInterval(renderTrajectoryOverlay, 250);
|
||||||
|
|
||||||
@@ -970,10 +1093,21 @@ window.addEventListener("load", function () {
|
|||||||
video.src = "//" + hostUrl + ":8083/stream.mjpg";
|
video.src = "//" + hostUrl + ":8083/stream.mjpg";
|
||||||
video.addEventListener("load", function () {
|
video.addEventListener("load", function () {
|
||||||
setText("cam-state", "Online");
|
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();
|
updateCameraFrameLayout();
|
||||||
});
|
});
|
||||||
video.addEventListener("error", function () {
|
video.addEventListener("error", function () {
|
||||||
setText("cam-state", "Fehler");
|
setText("cam-state", "Fehler");
|
||||||
|
setText("cam-live-text", "OFFLINE");
|
||||||
|
var liveIndicator = document.getElementById("cam-live-indicator");
|
||||||
|
if (liveIndicator) {
|
||||||
|
liveIndicator.classList.add("is-offline");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
updateCameraFrameLayout();
|
updateCameraFrameLayout();
|
||||||
window.addEventListener("resize", updateCameraFrameLayout);
|
window.addEventListener("resize", updateCameraFrameLayout);
|
||||||
@@ -1006,14 +1140,17 @@ window.addEventListener("load", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ros.on("connection", function () {
|
ros.on("connection", function () {
|
||||||
|
markRosConnection(true);
|
||||||
setRosStatus("Verbunden");
|
setRosStatus("Verbunden");
|
||||||
});
|
});
|
||||||
|
|
||||||
ros.on("error", function () {
|
ros.on("error", function () {
|
||||||
|
markRosConnection(false);
|
||||||
setRosStatus("Fehler");
|
setRosStatus("Fehler");
|
||||||
});
|
});
|
||||||
|
|
||||||
ros.on("close", function () {
|
ros.on("close", function () {
|
||||||
|
markRosConnection(false);
|
||||||
setRosStatus("Getrennt");
|
setRosStatus("Getrennt");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1074,6 +1211,7 @@ window.addEventListener("load", function () {
|
|||||||
messageType: "sensor_msgs/Joy"
|
messageType: "sensor_msgs/Joy"
|
||||||
});
|
});
|
||||||
joySubscriber.subscribe(function (message) {
|
joySubscriber.subscribe(function (message) {
|
||||||
|
markRosConnection(true);
|
||||||
if (message.header.frame_id === "webgui") return;
|
if (message.header.frame_id === "webgui") return;
|
||||||
var x = (message.axes[0] || 0);
|
var x = (message.axes[0] || 0);
|
||||||
var y = -(message.axes[1] || 0);
|
var y = -(message.axes[1] || 0);
|
||||||
@@ -1089,6 +1227,7 @@ window.addEventListener("load", function () {
|
|||||||
document.getElementById("motor-toggle").addEventListener("click", toggleMotors);
|
document.getElementById("motor-toggle").addEventListener("click", toggleMotors);
|
||||||
|
|
||||||
document.getElementById("mt-joy").addEventListener("click", function () {
|
document.getElementById("mt-joy").addEventListener("click", function () {
|
||||||
|
if (controlsAreBlocked()) return;
|
||||||
inputMode = "joystick";
|
inputMode = "joystick";
|
||||||
updateInputDisplay();
|
updateInputDisplay();
|
||||||
document.getElementById("mt-joy").classList.add("active");
|
document.getElementById("mt-joy").classList.add("active");
|
||||||
@@ -1100,6 +1239,7 @@ window.addEventListener("load", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("mt-dpad").addEventListener("click", function () {
|
document.getElementById("mt-dpad").addEventListener("click", function () {
|
||||||
|
if (controlsAreBlocked()) return;
|
||||||
inputMode = "dpad";
|
inputMode = "dpad";
|
||||||
updateInputDisplay();
|
updateInputDisplay();
|
||||||
document.getElementById("mt-dpad").classList.add("active");
|
document.getElementById("mt-dpad").classList.add("active");
|
||||||
@@ -1112,6 +1252,7 @@ window.addEventListener("load", function () {
|
|||||||
|
|
||||||
var joyBase = document.getElementById("jbase");
|
var joyBase = document.getElementById("jbase");
|
||||||
joyBase.addEventListener("mousedown", function (event) {
|
joyBase.addEventListener("mousedown", function (event) {
|
||||||
|
if (controlsAreBlocked()) return;
|
||||||
dragging = true;
|
dragging = true;
|
||||||
moveThumb(event.clientX, event.clientY);
|
moveThumb(event.clientX, event.clientY);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -1128,6 +1269,7 @@ window.addEventListener("load", function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
joyBase.addEventListener("touchstart", function (event) {
|
joyBase.addEventListener("touchstart", function (event) {
|
||||||
|
if (controlsAreBlocked()) return;
|
||||||
dragging = true;
|
dragging = true;
|
||||||
moveThumb(event.touches[0].clientX, event.touches[0].clientY);
|
moveThumb(event.touches[0].clientX, event.touches[0].clientY);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -1147,6 +1289,7 @@ window.addEventListener("load", function () {
|
|||||||
|
|
||||||
document.querySelectorAll(".dpad-btn").forEach(function (button) {
|
document.querySelectorAll(".dpad-btn").forEach(function (button) {
|
||||||
function activate(event) {
|
function activate(event) {
|
||||||
|
if (controlsAreBlocked()) return;
|
||||||
var x = parseFloat(button.dataset.x);
|
var x = parseFloat(button.dataset.x);
|
||||||
var y = parseFloat(button.dataset.y);
|
var y = parseFloat(button.dataset.y);
|
||||||
setDpadPressed(button, x, y);
|
setDpadPressed(button, x, y);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import errno
|
import errno
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
import termios
|
import termios
|
||||||
@@ -76,9 +77,15 @@ class GpsState(object):
|
|||||||
self.track_deg = None
|
self.track_deg = None
|
||||||
self.fix_quality = 0
|
self.fix_quality = 0
|
||||||
self.satellites = 0
|
self.satellites = 0
|
||||||
|
self.pdop = None
|
||||||
|
self.vdop = None
|
||||||
self.valid = False
|
self.valid = False
|
||||||
self.last_sentence_time = None
|
self.last_sentence_time = None
|
||||||
self.last_fix_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):
|
def summary_text(self):
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
@@ -200,15 +207,129 @@ def parse_rmc(fields, state):
|
|||||||
state.valid = True
|
state.valid = True
|
||||||
state.last_fix_time = rospy.Time.now()
|
state.last_fix_time = rospy.Time.now()
|
||||||
|
|
||||||
# Optional date/time parse for debugging consistency.
|
|
||||||
if fields[1] and fields[9]:
|
if fields[1] and fields[9]:
|
||||||
try:
|
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:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return True
|
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):
|
def parse_sentence(sentence, state):
|
||||||
if not verify_checksum(sentence):
|
if not verify_checksum(sentence):
|
||||||
return False
|
return False
|
||||||
@@ -224,10 +345,45 @@ def parse_sentence(sentence, state):
|
|||||||
return parse_gga(fields, state)
|
return parse_gga(fields, state)
|
||||||
if sentence_type.endswith('RMC'):
|
if sentence_type.endswith('RMC'):
|
||||||
return parse_rmc(fields, state)
|
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
|
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 = NavSatFix()
|
||||||
message.header.stamp = rospy.Time.now()
|
message.header.stamp = rospy.Time.now()
|
||||||
message.header.frame_id = frame_id
|
message.header.frame_id = frame_id
|
||||||
@@ -258,6 +414,7 @@ def publish_fix(fix_publisher, overlay_publisher, state, frame_id):
|
|||||||
|
|
||||||
fix_publisher.publish(message)
|
fix_publisher.publish(message)
|
||||||
overlay_publisher.publish(String(data=state.summary_text()))
|
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__':
|
if __name__ == '__main__':
|
||||||
@@ -270,6 +427,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
fix_publisher = rospy.Publisher('/fix', NavSatFix, queue_size=5)
|
fix_publisher = rospy.Publisher('/fix', NavSatFix, queue_size=5)
|
||||||
overlay_publisher = rospy.Publisher('/gps/status_text', String, 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()
|
state = GpsState()
|
||||||
serial_fd = None
|
serial_fd = None
|
||||||
@@ -280,14 +438,14 @@ if __name__ == '__main__':
|
|||||||
serial_fd = open_device(port, baud)
|
serial_fd = open_device(port, baud)
|
||||||
buffer = bytearray()
|
buffer = bytearray()
|
||||||
state.connected = serial_fd is not None
|
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:
|
if serial_fd is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
readable, _, _ = select.select([serial_fd], [], [], 0.5)
|
readable, _, _ = select.select([serial_fd], [], [], 0.5)
|
||||||
if serial_fd not in readable:
|
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
|
continue
|
||||||
|
|
||||||
chunk = os.read(serial_fd, READ_SIZE)
|
chunk = os.read(serial_fd, READ_SIZE)
|
||||||
@@ -302,7 +460,7 @@ if __name__ == '__main__':
|
|||||||
if not line or not line.startswith('$'):
|
if not line or not line.startswith('$'):
|
||||||
continue
|
continue
|
||||||
if parse_sentence(line, state):
|
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:
|
except OSError as exc:
|
||||||
if exc.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):
|
if exc.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):
|
||||||
|
|||||||
Reference in New Issue
Block a user