Files
ExoMy_Cuno/ExoMy_Software-master/gui/index.html
T
2026-05-21 19:30:33 +02:00

1201 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>ExoMy // Ground Control</title>
<script type="text/javascript" src="./vendor/roslib.min.js"></script>
<style>
:root {
--bg: #DCE3EC;
--pri: #0088FF;
--sec: #FF6B00;
--ok: #00C853;
--warn: #FFB300;
--txt: #0B1525;
--txt2: #506070;
--panel: rgba(255,255,255,0.93);
--border: rgba(0,136,255,0.22);
--mono: "Consolas", "Cascadia Code", "Cascadia Mono", "Courier New", monospace;
--disp: "Consolas", "Cascadia Code", "Cascadia Mono", "Courier New", monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
body {
font-family: var(--mono);
background-color: var(--bg);
background-image:
linear-gradient(rgba(0,136,255,.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,136,255,.06) 1px, transparent 1px);
background-size: 32px 32px;
color: var(--txt);
display: flex;
flex-direction: column;
}
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: 100;
}
.hdr-icon { width: 32px; height: 32px; flex-shrink: 0; }
.hdr-title {
font-family: var(--disp);
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; }
.mission-ok {
display: flex;
align-items: center;
gap: 6px;
font-size: 8px;
color: var(--ok);
letter-spacing: 2px;
text-transform: uppercase;
}
.pulse-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--ok);
animation: pdot 2s ease-in-out infinite;
box-shadow: 0 0 0 0 rgba(0,200,83,.5);
}
@keyframes pdot {
0% { box-shadow: 0 0 0 0 rgba(0,200,83,.5); }
70% { box-shadow: 0 0 0 7px rgba(0,200,83,0); }
100% { box-shadow: 0 0 0 0 rgba(0,200,83,0); }
}
.hdr-clocks { display: flex; gap: 16px; }
.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-family: var(--disp);
font-weight: 700;
font-size: 14px;
color: #fff;
letter-spacing: 3px;
line-height: 1.1;
}
.hdr-clk-val.ora { color: var(--sec); }
.panel {
position: relative;
background:
linear-gradient(#0088FF,#0088FF) 0 0 / 14px 2px no-repeat,
linear-gradient(#0088FF,#0088FF) 0 0 / 2px 14px no-repeat,
linear-gradient(#0088FF,#0088FF) 100% 0 / 14px 2px no-repeat,
linear-gradient(#0088FF,#0088FF) 100% 0 / 2px 14px no-repeat,
linear-gradient(#0088FF,#0088FF) 0 100% / 14px 2px no-repeat,
linear-gradient(#0088FF,#0088FF) 0 100% / 2px 14px no-repeat,
linear-gradient(#0088FF,#0088FF) 100% 100% / 14px 2px no-repeat,
linear-gradient(#0088FF,#0088FF) 100% 100% / 2px 14px no-repeat,
rgba(255,255,255,0.93);
border: 1px solid var(--border);
box-shadow: 0 1px 14px rgba(0,0,0,.06);
}
.p-hdr {
display: flex;
align-items: center;
gap: 7px;
padding: 7px 12px;
border-bottom: 1px solid rgba(0,136,255,.15);
background: linear-gradient(90deg, rgba(0,136,255,.07) 0%, transparent 100%);
}
.p-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--pri); flex-shrink: 0; }
.p-lbl { font-size: 8px; letter-spacing: 3px; color: var(--pri); text-transform: uppercase; font-weight: bold; }
.p-tag { margin-left: auto; font-size: 7px; color: var(--txt2); letter-spacing: 1px; }
.main {
flex: 1;
display: grid;
grid-template-columns: 1fr clamp(230px, 28vw, 330px);
gap: clamp(6px, 1vw, 12px);
padding: clamp(6px, 1vw, 12px);
min-height: 0;
overflow: hidden;
}
.col-l, .col-r {
display: flex;
flex-direction: column;
gap: clamp(6px, 1vw, 12px);
min-height: 0;
}
#drive-panel,
#stat-panel { flex-shrink: 0; }
.drive-btns { display: flex; gap: 8px; padding: 10px 12px; }
.dbtn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 6px;
border: 1px solid rgba(0,136,255,.28);
background: transparent;
color: var(--txt2);
font-family: var(--mono);
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
cursor: pointer;
transition: all .18s ease;
}
.dbtn:hover {
border-color: var(--pri);
color: var(--pri);
background: rgba(0,136,255,.06);
}
.dbtn.active {
border-color: var(--pri);
background: var(--pri);
color: #fff;
box-shadow: 0 0 24px rgba(0,136,255,.45), inset 0 0 16px rgba(255,255,255,.1);
}
@keyframes mode-flash {
0% { box-shadow: 0 0 0px rgba(0,255,145,0), background-color: var(--pri); }
30% { box-shadow: 0 0 28px rgba(0,255,145,.9), background-color: #00ff91; }
100% { box-shadow: 0 0 24px rgba(0,136,255,.45), background-color: var(--pri); }
}
.dbtn.flash { animation: mode-flash .5s ease-out forwards; }
#cam-panel { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.cam-port {
flex: 1;
position: relative;
overflow: hidden;
background: #0C0E14;
min-height: 0;
}
#video_output {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
background: #05070d;
z-index: 1;
}
.c-vignette {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 6;
background: radial-gradient(ellipse at 50% 50%, transparent 52%, rgba(0,0,0,.58) 100%);
}
.c-xhair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 180px;
height: 180px;
z-index: 10;
pointer-events: none;
}
@keyframes xh-spin { to { transform: rotate(360deg); } }
@keyframes xh-pulse {
0% { transform: scale(1); opacity: .7; }
100% { transform: scale(2.8); opacity: 0; }
}
.xh-rot { animation: xh-spin 14s linear infinite; transform-box: fill-box; transform-origin: center; }
.xh-rot-rev { animation: xh-spin 8s linear infinite reverse; transform-box: fill-box; transform-origin: center; }
.xh-pulse { animation: xh-pulse 2.5s ease-out infinite; transform-box: fill-box; transform-origin: center; }
.c-rec {
position: absolute;
top: 13px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 5px;
font-size: 8px;
color: rgba(255,55,55,.9);
letter-spacing: 2px;
z-index: 10;
}
.c-rec-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #FF3333;
animation: blink 1.1s step-end infinite;
}
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0; } }
.c-tl, .c-tr, .c-bl, .c-br {
position: absolute;
z-index: 10;
font-size: 9px;
color: rgba(0,255,145,.72);
letter-spacing: 1px;
line-height: 1.9;
pointer-events: none;
}
.c-tl { top: 13px; left: 13px; }
.c-tr { top: 13px; right: 13px; text-align: right; }
.c-bl { bottom: 13px; left: 13px; }
.c-br { bottom: 13px; right: 13px; text-align: right; }
.stat-list {
padding: 8px 12px 10px;
display: flex;
flex-direction: column;
gap: 5px;
}
.srow {
display: flex;
align-items: center;
padding: 5px 8px;
background: rgba(0,136,255,.025);
border-left: 2px solid transparent;
}
.srow.ok { border-left-color: var(--ok); }
.srow.warn { border-left-color: var(--sec); }
.srow.info { border-left-color: var(--pri); }
.slbl {
font-size: 7px;
letter-spacing: 2px;
color: var(--txt2);
text-transform: uppercase;
flex: 1;
}
.sval { font-size: 11px; font-weight: bold; letter-spacing: .5px; }
.sval.g { color: var(--ok); }
.sval.o { color: var(--sec); }
.sval.b { color: var(--pri); }
.sval.alert {
color: #D50000;
animation: uv-blink 0.9s step-end infinite;
}
.sval.warntext { color: var(--sec); }
.sval-conn { display: flex; align-items: center; gap: 5px; color: var(--ok); font-size: 10px; }
.sbar-wrap { display: flex; align-items: center; gap: 8px; }
.sbar { width: 52px; height: 3px; background: rgba(0,136,255,.12); border-radius: 1px; overflow: hidden; }
.sbar-i { height: 100%; border-radius: 1px; background: var(--pri); }
.sbar-i.w { background: var(--sec); }
@keyframes uv-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; }
}
#joy-panel .p-hdr { padding: 14px 12px; }
.mode-toggle { display: flex; gap: 2px; margin-left: auto; }
.mtbtn {
font-family: var(--mono);
font-size: 8px;
letter-spacing: 1.5px;
padding: 6px 14px;
border: 1px solid rgba(0,136,255,.3);
background: transparent;
color: var(--txt2);
cursor: pointer;
transition: all .15s;
text-transform: uppercase;
}
.mtbtn.active { background: var(--pri); color: #fff; border-color: var(--pri); }
#joy-panel { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.joy-area,
.dpad-wrap {
flex: 1;
align-items: center;
justify-content: center;
gap: 10px;
padding: 12px;
min-height: 0;
overflow: hidden;
}
.joy-area { display: flex; flex-direction: column; }
.joy-base {
width: min(calc(100% - 28px), 40vh, 200px);
height: min(calc(100% - 28px), 40vh, 200px);
border-radius: 50%;
background: radial-gradient(circle, rgba(0,136,255,.08) 0%, rgba(0,136,255,.02) 60%, rgba(0,136,255,.05) 100%);
border: 1.5px solid rgba(0,136,255,.32);
box-shadow: inset 0 0 32px rgba(0,136,255,.06);
position: relative;
touch-action: none;
user-select: none;
cursor: grab;
}
.joy-base:active { cursor: grabbing; }
.jg-h, .jg-v { position: absolute; background: rgba(0,136,255,.13); }
.jg-h { top: 50%; left: 11%; right: 11%; height: 1px; transform: translateY(-50%); }
.jg-v { left: 50%; top: 11%; bottom: 11%; width: 1px; transform: translateX(-50%); }
.jring {
position: absolute;
border-radius: 50%;
border: 1px solid rgba(0,136,255,.11);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.jr1 { width: 58px; height: 58px; }
.jr2 { width: 116px; height: 116px; }
.joy-thumb {
position: absolute;
width: 58px;
height: 58px;
border-radius: 50%;
background: radial-gradient(circle at 36% 32%, #55C0FF, #007EEE, #004499);
border: 2px solid rgba(255,255,255,.35);
box-shadow: 0 4px 20px rgba(0,136,255,.6), 0 0 0 4px rgba(0,136,255,.12);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
.joy-thumb::after {
content: "";
position: absolute;
top: 9px;
left: 9px;
width: 9px;
height: 9px;
border-radius: 50%;
background: rgba(255,255,255,.44);
}
.dpad-wrap { display: none; flex-direction: column; }
.dpad-wrap.visible { display: flex; }
.dpad-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 5px;
width: min(156px, calc(100% - 28px));
aspect-ratio: 1;
}
.dpad-btn {
background: rgba(0,136,255,.06);
border: 1px solid rgba(0,136,255,.28);
color: var(--txt2);
font-size: 15px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all .1s;
touch-action: none;
user-select: none;
border-radius: 2px;
}
.dpad-btn:hover { border-color: var(--pri); color: var(--pri); background: rgba(0,136,255,.1); }
.dpad-btn.pressed {
background: var(--pri);
border-color: var(--pri);
color: #fff;
box-shadow: 0 0 18px rgba(0,136,255,.5);
}
.dpad-mid {
background: rgba(0,136,255,.04);
border: 1px solid rgba(0,136,255,.12);
border-radius: 2px;
}
.dpad-diag { font-size: 12px; opacity: .75; }
.dpad-diag:hover { opacity: 1; }
.dpad-diag.pressed { opacity: 1; }
.joy-vals { display: flex; gap: 22px; }
.joy-ax { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.joy-ax-n { font-size: 7px; letter-spacing: 2px; color: var(--pri); text-transform: uppercase; }
.joy-ax-v {
font-family: var(--disp);
font-weight: 700;
font-size: 14px;
color: var(--txt);
min-width: 56px;
text-align: center;
letter-spacing: 2px;
}
.admin-link {
color: rgba(255,255,255,.75);
text-decoration: none;
font-size: 8px;
letter-spacing: 2px;
text-transform: uppercase;
}
.admin-link:hover { color: #fff; }
@media (max-width: 980px) {
body { overflow-y: auto; }
html, body { overflow-x: hidden; }
.main {
grid-template-columns: 1fr;
overflow: visible;
}
.drive-btns { flex-direction: column; }
}
</style>
</head>
<body>
<header>
<svg class="hdr-icon" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="11" width="18" height="10" rx="1" fill="#0088FF" opacity=".85"/>
<rect x="11" y="7" width="10" height="4" rx="1" fill="#0088FF" opacity=".5"/>
<circle cx="9" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="23" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="9" cy="23" r="1.3" fill="#0088FF"/>
<circle cx="23" cy="23" r="1.3" fill="#0088FF"/>
<rect x="22" y="8" width="4" height="3" rx=".5" fill="#FF6B00" opacity=".9"/>
<line x1="16" y1="7" x2="16" y2="3" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="16" cy="2.5" r="1" fill="#0088FF"/>
<rect x="3" y="14" width="4" height="2" rx="1" fill="#0088FF" opacity=".6"/>
</svg>
<span class="hdr-title">EXO<em>MY</em>&nbsp;GROUND CTRL</span>
<div class="hdr-div"></div>
<div class="hdr-badge">Mission Ops</div>
<div class="hdr-gap"></div>
<a class="admin-link" href="./admin.html">Admin</a>
<div class="hdr-div"></div>
<div class="mission-ok"><div class="pulse-dot"></div><span id="mission-state">Bereit</span></div>
<div class="hdr-div"></div>
<div class="hdr-clocks">
<div class="hdr-clk">
<div class="hdr-clk-lbl">Session</div>
<div class="hdr-clk-val ora" id="met">00:00:00</div>
</div>
<div class="hdr-div"></div>
<div class="hdr-clk">
<div class="hdr-clk-lbl">UTC</div>
<div class="hdr-clk-val" id="utc">--:--:--</div>
</div>
</div>
</header>
<div class="main">
<div class="col-l">
<div class="panel" id="drive-panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Fahrmodus</span>
<span class="p-tag">Mode Select</span>
</div>
<div class="drive-btns">
<button class="dbtn active" data-mode="ackermann" data-button="1">Ackermann</button>
<button class="dbtn" data-mode="turn-on-point" data-button="0">Turn on Point</button>
<button class="dbtn" data-mode="crab" data-button="3">Crab</button>
<button class="dbtn" id="motor-toggle">Motoren</button>
</div>
</div>
<div class="panel" id="cam-panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Kamera — CAM01</span>
<span class="p-tag">Live Stream</span>
</div>
<div class="cam-port">
<img id="video_output" alt="Kamerabild vom ExoMy" />
<div class="c-rec"><div class="c-rec-dot"></div>LIVE</div>
<div class="c-tl">
CAM: <span id="cam-state">Verbinde…</span><br>
MODE: <span id="overlay-mode">Ackermann</span>
</div>
<div class="c-tr">
ROS: <span id="ros-state">Offline</span><br>
CTRL: <span id="input-state">Joystick</span>
</div>
<div class="c-bl">
X: <span id="cam-x">+0.00</span><br>
Y: <span id="cam-y">+0.00</span>
</div>
<div class="c-br">
HOST: <span id="cam-host">--</span><br>
STREAM: 8081
</div>
<div class="c-xhair">
<svg viewBox="-90 -90 180 180" xmlns="http://www.w3.org/2000/svg" width="180" height="180">
<!-- äußerer rotierender Strichelring -->
<circle class="xh-rot" cx="0" cy="0" r="82" fill="none" stroke="rgba(0,255,145,.2)" stroke-width="1" stroke-dasharray="4 10"/>
<!-- mittlerer Ring, Gegenrichtung -->
<circle class="xh-rot-rev" cx="0" cy="0" r="60" fill="none" stroke="rgba(0,255,145,.15)" stroke-width=".75" stroke-dasharray="2 16"/>
<!-- innerer fester Ring -->
<circle cx="0" cy="0" r="16" fill="none" stroke="rgba(0,255,145,.3)" stroke-width=".75"/>
<!-- Kreuzlinien mit Lücke in der Mitte -->
<line x1="-86" y1="0" x2="-19" y2="0" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
<line x1="19" y1="0" x2="86" y2="0" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
<line x1="0" y1="-86" x2="0" y2="-19" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
<line x1="0" y1="19" x2="0" y2="86" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
<!-- Skalenstriche bei r=60 -->
<line x1="-60" y1="-6" x2="-60" y2="6" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
<line x1="60" y1="-6" x2="60" y2="6" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
<line x1="-6" y1="-60" x2="6" y2="-60" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
<line x1="-6" y1="60" x2="6" y2="60" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
<!-- Skalenstriche bei r=38 -->
<line x1="-38" y1="-3.5" x2="-38" y2="3.5" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
<line x1="38" y1="-3.5" x2="38" y2="3.5" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
<line x1="-3.5" y1="-38" x2="3.5" y2="-38" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
<line x1="-3.5" y1="38" x2="3.5" y2="38" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
<!-- Eckklammern -->
<path d="M-74,-58 L-74,-74 L-58,-74" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
<path d="M58,-74 L74,-74 L74,-58" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
<path d="M-74,58 L-74,74 L-58,74" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
<path d="M58,74 L74,74 L74,58" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
<!-- pulsierender Ring -->
<circle class="xh-pulse" cx="0" cy="0" r="6" fill="none" stroke="rgba(0,255,145,.7)" stroke-width="1"/>
<!-- Mittelpunkt -->
<circle cx="0" cy="0" r="2.5" fill="rgba(0,255,145,.9)"/>
<circle cx="0" cy="0" r="1" fill="rgba(255,255,255,.7)"/>
</svg>
</div>
<div class="c-vignette"></div>
</div>
</div>
</div>
<div class="col-r">
<div class="panel" id="stat-panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Systemstatus</span>
<span class="p-tag">Web Control</span>
</div>
<div class="stat-list">
<div class="srow ok">
<span class="slbl">Verbindung</span>
<span class="sval">
<span class="sval-conn"><span class="pulse-dot"></span><span id="status">Verbinde…</span></span>
</span>
</div>
<div class="srow info">
<span class="slbl">Aktiver Modus</span>
<span class="sval b" id="stat-mode">Ackermann</span>
</div>
<div class="srow warn">
<span class="slbl">Motoren</span>
<span class="sval o" id="stat-motors">Unbekannt</span>
</div>
<div class="srow info">
<span class="slbl">Eingabe</span>
<span class="sval b" id="stat-input">Joystick</span>
</div>
<div class="srow info">
<span class="slbl">CPU Last</span>
<span class="sval b">
<div class="sbar-wrap">
<div class="sbar"><div class="sbar-i" id="bar-cpu" style="width:0%"></div></div>
<span id="val-cpu">--</span>
</div>
</span>
</div>
<div class="srow warn">
<span class="slbl">CPU Temp</span>
<span class="sval b">
<div class="sbar-wrap">
<div class="sbar"><div class="sbar-i w" id="bar-temp" style="width:0%"></div></div>
<span id="val-temp">--</span>
</div>
</span>
</div>
<div class="srow info">
<span class="slbl">RAM Last</span>
<span class="sval b">
<div class="sbar-wrap">
<div class="sbar"><div class="sbar-i" id="bar-mem" style="width:0%"></div></div>
<span id="val-mem">--</span>
</div>
</span>
</div>
<div class="srow info">
<span class="slbl">WLAN</span>
<span class="sval b" id="stat-wifi">--</span>
</div>
<div class="srow info">
<span class="slbl">IP Adressen</span>
<span class="sval b" id="stat-ips">--</span>
</div>
<div class="srow warn">
<span class="slbl">Unterspannung</span>
<span class="sval b" id="stat-undervoltage">--</span>
</div>
<div class="srow info">
<span class="slbl">Uptime Pi</span>
<span class="sval b" id="stat-uptime">--</span>
</div>
<div class="srow info">
<span class="slbl">Speicher Frei</span>
<span class="sval b" id="stat-disk">--</span>
</div>
</div>
</div>
<div class="panel" id="joy-panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Lokomotion</span>
<div class="mode-toggle">
<button class="mtbtn active" id="mt-joy">Joystick</button>
<button class="mtbtn" id="mt-dpad">D-Pad</button>
</div>
</div>
<div class="joy-area" id="joy-area">
<div class="joy-base" id="jbase">
<div class="jg-h"></div><div class="jg-v"></div>
<div class="jring jr1"></div>
<div class="jring jr2"></div>
<div class="joy-thumb" id="jthumb"></div>
</div>
<div class="joy-vals">
<div class="joy-ax">
<div class="joy-ax-n" id="joy-x-label">X-Achse</div>
<div class="joy-ax-v" id="jx">+0.00</div>
</div>
<div class="joy-ax">
<div class="joy-ax-n">Y-Achse</div>
<div class="joy-ax-v" id="jy">+0.00</div>
</div>
</div>
</div>
<div class="dpad-wrap" id="dpad-area">
<div class="dpad-grid">
<button class="dpad-btn dpad-diag" data-x="-0.71" data-y="0.71">↖</button>
<button class="dpad-btn" data-x="0" data-y="1">▲</button>
<button class="dpad-btn dpad-diag" data-x="0.71" data-y="0.71">↗</button>
<button class="dpad-btn" data-x="-1" data-y="0">◄</button>
<div class="dpad-mid"></div>
<button class="dpad-btn" data-x="1" data-y="0">►</button>
<button class="dpad-btn dpad-diag" data-x="-0.71" data-y="-0.71">↙</button>
<button class="dpad-btn" data-x="0" data-y="-1">▼</button>
<button class="dpad-btn dpad-diag" data-x="0.71" data-y="-0.71">↘</button>
</div>
<div class="joy-vals">
<div class="joy-ax">
<div class="joy-ax-n" id="dpad-x-label">X-Achse</div>
<div class="joy-ax-v" id="dpx">+0.00</div>
</div>
<div class="joy-ax">
<div class="joy-ax-n">Y-Achse</div>
<div class="joy-ax-v" id="dpy">+0.00</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
var hostUrl = window.location.hostname;
var ros = null;
var joyListener = null;
var roverCommandListener = null;
var publishTimer = null;
var statusTimer = null;
var axes = [0, 0, 0, 0, 0, 0];
var buttons = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
var driveMode = "ackermann";
var motorsEnabled = true;
var inputMode = "joystick";
var metBase = Date.now();
var dragging = false;
function setText(id, text) {
var el = document.getElementById(id);
if (el) {
el.textContent = text;
}
}
function formatAxis(value) {
return (value >= 0 ? "+" : "") + value.toFixed(2);
}
function axisBarWidth(value) {
return Math.max(0, Math.min(100, ((value + 1) / 2) * 100)).toFixed(0) + "%";
}
function percentTextToWidth(value) {
if (!value || typeof value !== "string") {
return "0%";
}
var numeric = parseInt(value.replace("%", ""), 10);
if (isNaN(numeric)) {
return "0%";
}
return Math.max(0, Math.min(100, numeric)) + "%";
}
function temperatureTextToWidth(value) {
if (!value || typeof value !== "string") {
return "0%";
}
var numeric = parseFloat(value.replace("°C", "").replace(",", "."));
if (isNaN(numeric)) {
return "0%";
}
return Math.max(0, Math.min(100, ((numeric - 20) / 60) * 100)).toFixed(0) + "%";
}
function updateAxesDisplay(x, y) {
setText("jx", formatAxis(x));
setText("jy", formatAxis(y));
setText("dpx", formatAxis(x));
setText("dpy", formatAxis(y));
setText("cam-x", formatAxis(x));
setText("cam-y", formatAxis(y));
}
function updateModeDisplay() {
var label = "Ackermann";
if (driveMode === "turn-on-point") {
label = "Turn on Point";
} else if (driveMode === "crab") {
label = "Crab";
}
setText("overlay-mode", label);
setText("stat-mode", label);
}
function applyDriveMode(mode, flash) {
var changed = mode !== driveMode;
driveMode = mode;
document.querySelectorAll(".dbtn[data-mode]").forEach(function (btn) {
var isActive = btn.dataset.mode === mode;
btn.classList.toggle("active", isActive);
if (isActive && changed && flash) {
btn.classList.remove("flash");
void btn.offsetWidth;
btn.classList.add("flash");
}
});
updateModeDisplay();
updateControlLocks();
}
function mirrorJoyThumb(x, y) {
if (dragging || inputMode !== "joystick") return;
var data = joyMetrics();
var dx = x * data.maxD;
var dy = -y * data.maxD;
var thumb = document.getElementById("jthumb");
thumb.style.transition = "left .05s, top .05s";
thumb.style.left = "calc(50% + " + dx + "px)";
thumb.style.top = "calc(50% + " + dy + "px)";
thumb.style.transform = "translate(-50%,-50%)";
updateAxesDisplay(x, y);
}
function locomotionModeToUi(modeValue) {
if (modeValue === 2) {
return "turn-on-point";
}
if (modeValue === 3) {
return "crab";
}
return "ackermann";
}
function updateMotorDisplay() {
setText("stat-motors", motorsEnabled ? "Aktiv" : "Aus");
}
function updateInputDisplay() {
var label = inputMode === "joystick" ? "Joystick" : "D-Pad";
setText("input-state", label);
setText("stat-input", label);
}
function setRosStatus(text) {
setText("status", text);
setText("ros-state", text);
setText("mission-state", text === "Verbunden" ? "Nominal" : text);
}
function updateUndervoltageDisplay(text, state) {
var el = document.getElementById("stat-undervoltage");
if (!el) {
return;
}
el.textContent = text || "--";
el.classList.remove("b", "g", "o", "alert", "warntext");
if (state === "active") {
el.classList.add("alert");
return;
}
if (state === "past") {
el.classList.add("warntext");
return;
}
if (state === "clear") {
el.classList.add("g");
return;
}
el.classList.add("b");
}
function updateServiceState(payload) {
var system = payload.system || {};
var services = payload.services || {};
setText("stat-wifi", system.wifi || "unbekannt");
setText("stat-ips", system.ips || "unbekannt");
updateUndervoltageDisplay(system.undervoltage || "unbekannt", system.undervoltage_state || "unknown");
setText("stat-uptime", system.uptime || "unbekannt");
setText("stat-disk", system.disk_free || "unbekannt");
setText("val-cpu", system.cpu_usage || "--");
setText("val-temp", system.cpu_temperature || "--");
setText("val-mem", system.memory_usage || "--");
document.getElementById("bar-cpu").style.width = percentTextToWidth(system.cpu_usage);
document.getElementById("bar-mem").style.width = percentTextToWidth(system.memory_usage);
document.getElementById("bar-temp").style.width = temperatureTextToWidth(system.cpu_temperature);
setText("cam-state", services.camera === "active" ? "Online" : services.camera || "unbekannt");
}
function fetchSystemStatus() {
fetch(window.location.protocol + "//" + hostUrl + ":8082/api/status", {
cache: "no-store"
}).then(function (response) {
if (!response.ok) {
throw new Error("status");
}
return response.json();
}).then(function (payload) {
updateServiceState(payload);
}).catch(function () {
setText("stat-wifi", "nicht erreichbar");
});
}
function publishJoy() {
if (!joyListener) {
return;
}
joyListener.publish(new ROSLIB.Message({
header: {
frame_id: "webgui"
},
axes: axes,
buttons: buttons
}));
}
function startPublishing() {
if (publishTimer) {
return;
}
publishJoy();
publishTimer = window.setInterval(publishJoy, 50);
}
function stopPublishing() {
if (publishTimer) {
clearInterval(publishTimer);
publishTimer = null;
}
}
function setAxes(x, y) {
axes = [x, y, 0, 0, 0, 0];
updateAxesDisplay(x, y);
}
function resetAxes() {
setAxes(0, 0);
publishJoy();
}
function pulseButton(buttonIndex) {
axes = [0, 0, 0, 0, 0, 0];
updateAxesDisplay(0, 0);
buttons[buttonIndex] = 1;
publishJoy();
window.setTimeout(function () {
buttons[buttonIndex] = 0;
publishJoy();
}, 120);
}
function selectDriveMode(mode, buttonIndex) {
applyDriveMode(mode);
pulseButton(buttonIndex);
}
function toggleMotors() {
motorsEnabled = !motorsEnabled;
updateMotorDisplay();
pulseButton(9);
}
function updateControlLocks() {
var xOnly = driveMode === "turn-on-point";
setText("joy-x-label", driveMode === "turn-on-point" ? "Rotation" : "X-Achse");
setText("dpad-x-label", driveMode === "turn-on-point" ? "Rotation" : "X-Achse");
document.querySelectorAll("#joy-area .joy-ax")[1].style.opacity = xOnly ? "0.25" : "1";
document.querySelectorAll("#dpad-area .joy-ax")[1].style.opacity = xOnly ? "0.25" : "1";
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
var x = parseFloat(btn.dataset.x || "0");
var y = parseFloat(btn.dataset.y || "0");
var disable = xOnly && y !== 0;
btn.disabled = disable;
btn.style.opacity = disable ? "0.15" : "";
btn.style.pointerEvents = disable ? "none" : "";
});
}
function updateClocks() {
var now = new Date();
setText("utc", now.toUTCString().split(" ")[4]);
var s = Math.floor((Date.now() - metBase) / 1000);
var h = String(Math.floor(s / 3600)).padStart(2, "0");
var m = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
var sec = String(s % 60).padStart(2, "0");
setText("met", h + ":" + m + ":" + sec);
}
function joyMetrics() {
var rect = document.getElementById("jbase").getBoundingClientRect();
var baseR = Math.min(rect.width, rect.height) / 2;
var thumbR = baseR * 0.33;
return {
cx: rect.left + rect.width / 2,
cy: rect.top + rect.height / 2,
maxD: baseR - thumbR
};
}
function moveThumb(clientX, clientY) {
var data = joyMetrics();
var dx = clientX - data.cx;
var dy = clientY - data.cy;
if (driveMode === "turn-on-point" || driveMode === "crab") {
dy = 0;
}
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > data.maxD && distance > 0) {
dx = dx / distance * data.maxD;
dy = dy / distance * data.maxD;
}
var thumb = document.getElementById("jthumb");
thumb.style.left = "calc(50% + " + dx + "px)";
thumb.style.top = "calc(50% + " + dy + "px)";
thumb.style.transform = "translate(-50%,-50%)";
thumb.style.transition = "none";
var nx = dx / data.maxD;
var ny = -dy / data.maxD;
setAxes(nx, ny);
publishJoy();
startPublishing();
}
function resetThumb() {
var thumb = document.getElementById("jthumb");
thumb.style.transition = "left .22s cubic-bezier(.25,.46,.45,.94), top .22s cubic-bezier(.25,.46,.45,.94)";
thumb.style.left = "50%";
thumb.style.top = "50%";
thumb.style.transform = "translate(-50%,-50%)";
stopPublishing();
resetAxes();
}
function setDpadPressed(button, x, y) {
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
btn.classList.remove("pressed");
});
button.classList.add("pressed");
setAxes(x, y);
startPublishing();
}
function clearDpad() {
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
btn.classList.remove("pressed");
});
stopPublishing();
resetAxes();
}
window.addEventListener("load", function () {
setText("cam-host", hostUrl);
updateModeDisplay();
updateMotorDisplay();
updateInputDisplay();
updateAxesDisplay(0, 0);
updateControlLocks();
updateClocks();
window.setInterval(updateClocks, 1000);
fetchSystemStatus();
statusTimer = window.setInterval(fetchSystemStatus, 5000);
var video = document.getElementById("video_output");
video.src = "//" + hostUrl + ":8081/stream.mjpg";
video.addEventListener("load", function () {
setText("cam-state", "Online");
});
video.addEventListener("error", function () {
setText("cam-state", "Fehler");
});
ros = new ROSLIB.Ros({
url: "ws://" + hostUrl + ":9090"
});
ros.on("connection", function () {
setRosStatus("Verbunden");
});
ros.on("error", function () {
setRosStatus("Fehler");
});
ros.on("close", function () {
setRosStatus("Getrennt");
});
joyListener = new ROSLIB.Topic({
ros: ros,
name: "/joy",
messageType: "sensor_msgs/Joy"
});
roverCommandListener = new ROSLIB.Topic({
ros: ros,
name: "/rover_command",
messageType: "exomy/RoverCommand"
});
roverCommandListener.subscribe(function (message) {
applyDriveMode(locomotionModeToUi(message.locomotion_mode), true);
motorsEnabled = !!message.motors_enabled;
updateMotorDisplay();
});
var joySubscriber = new ROSLIB.Topic({
ros: ros,
name: "/joy",
messageType: "sensor_msgs/Joy"
});
joySubscriber.subscribe(function (message) {
if (message.header.frame_id === "webgui") return;
var x = (message.axes[0] || 0);
var y = -(message.axes[1] || 0);
mirrorJoyThumb(x, y);
});
document.querySelectorAll(".dbtn[data-mode]").forEach(function (button) {
button.addEventListener("click", function () {
selectDriveMode(button.dataset.mode, parseInt(button.dataset.button, 10));
});
});
document.getElementById("motor-toggle").addEventListener("click", toggleMotors);
document.getElementById("mt-joy").addEventListener("click", function () {
inputMode = "joystick";
updateInputDisplay();
document.getElementById("mt-joy").classList.add("active");
document.getElementById("mt-dpad").classList.remove("active");
document.getElementById("joy-area").style.display = "";
document.getElementById("dpad-area").classList.remove("visible");
clearDpad();
resetThumb();
});
document.getElementById("mt-dpad").addEventListener("click", function () {
inputMode = "dpad";
updateInputDisplay();
document.getElementById("mt-dpad").classList.add("active");
document.getElementById("mt-joy").classList.remove("active");
document.getElementById("joy-area").style.display = "none";
document.getElementById("dpad-area").classList.add("visible");
resetThumb();
clearDpad();
});
var joyBase = document.getElementById("jbase");
joyBase.addEventListener("mousedown", function (event) {
dragging = true;
moveThumb(event.clientX, event.clientY);
event.preventDefault();
});
document.addEventListener("mousemove", function (event) {
if (dragging) {
moveThumb(event.clientX, event.clientY);
}
});
document.addEventListener("mouseup", function () {
if (dragging) {
dragging = false;
resetThumb();
}
});
joyBase.addEventListener("touchstart", function (event) {
dragging = true;
moveThumb(event.touches[0].clientX, event.touches[0].clientY);
event.preventDefault();
}, { passive: false });
document.addEventListener("touchmove", function (event) {
if (dragging) {
moveThumb(event.touches[0].clientX, event.touches[0].clientY);
event.preventDefault();
}
}, { passive: false });
document.addEventListener("touchend", function () {
if (dragging) {
dragging = false;
resetThumb();
}
});
document.querySelectorAll(".dpad-btn").forEach(function (button) {
function activate(event) {
var x = parseFloat(button.dataset.x);
var y = parseFloat(button.dataset.y);
setDpadPressed(button, x, y);
if (event) {
event.preventDefault();
}
}
function deactivate() {
clearDpad();
}
button.addEventListener("mousedown", activate);
button.addEventListener("mouseup", deactivate);
button.addEventListener("mouseleave", deactivate);
button.addEventListener("touchstart", activate, { passive: false });
button.addEventListener("touchend", deactivate);
});
});
</script>
</body>
</html>