Private
Public Access
working camera movement
This commit is contained in:
1 parent
fce8f62534
commit
11e0875a20
21 files changed
+2117
-334
No files matched your search
+50
-228
@@ -27,6 +27,7 @@
|
||||
button.danger:hover { background: #9c352c; }
|
||||
button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
a.download { display: inline-block; text-decoration: none; }
|
||||
nav a[aria-current="page"] { background:#46695b; border:2px solid #c4e0cb; font-weight:700; }
|
||||
.value { font-weight: 650; color: #ffca78; }
|
||||
.notice { border-left: 4px solid #ffb44f; padding: .55rem .8rem; background: #302b21; margin: .8rem 0; }
|
||||
.muted { color: #bfcbc6; }
|
||||
@@ -40,12 +41,44 @@
|
||||
.event-image { display: block; max-width: 200px; max-height: 150px; border-radius: 7px; margin: .5rem 0; }
|
||||
.event-tools { margin: .3rem 0 .8rem; }
|
||||
@media (max-width: 760px) { main { grid-template-columns: 1fr; } }
|
||||
[hidden] { display: none !important; }
|
||||
header { padding: .8rem 2vw; display: grid; grid-template-columns: 1fr auto; gap: .25rem 1rem; align-items: center; position: sticky; top: 0; z-index: 20; }
|
||||
header h1, header > p { grid-column: 1; }
|
||||
header nav { grid-column: 2; grid-row: 1 / span 2; margin: 0 !important; }
|
||||
#all-stop-status:empty { display: none; }
|
||||
#all-stop-status { grid-column: 1 / -1; }
|
||||
main { width: 100%; max-width: 1800px; padding: 1rem 2vw; grid-template-columns: minmax(0,1.35fr) minmax(360px,1fr); align-items: start; }
|
||||
section { min-width: 0; }
|
||||
p { line-height: 1.45; }
|
||||
summary { cursor: pointer; font-weight: 650; padding: .65rem 0; }
|
||||
summary:focus-visible { outline: 3px solid #ffb44f; outline-offset: 3px; }
|
||||
|
||||
.camera-wrap { max-width: 720px; margin: auto; }
|
||||
.events { grid-column: auto; }
|
||||
details > summary { font-size: 1.1rem; }
|
||||
@media (max-width: 999px) {
|
||||
header { position: static; }
|
||||
main { grid-template-columns: minmax(0,1fr); }
|
||||
}
|
||||
@media (max-width: 580px) {
|
||||
header { display: block; padding: .8rem; }
|
||||
header nav { margin-top: .8rem !important; }
|
||||
main { padding: .7rem; }
|
||||
section { padding: .8rem; }
|
||||
button, select, input { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Halloween-Grab</h1>
|
||||
<p>Lokale Kameraansicht und Steuerung</p>
|
||||
<p>Auslösung, Ereignisse und Profile</p>
|
||||
<nav class="row" aria-label="Bereich wählen" style="margin-top:1rem">
|
||||
<a class="download" href="/grab" aria-current="page">Grab</a>
|
||||
<a class="download" href="/kamera" >Kamera / Finger</a>
|
||||
<button class="danger" id="all-stop">Alles stoppen</button>
|
||||
</nav>
|
||||
<p id="all-stop-status" role="status"></p>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
@@ -54,7 +87,7 @@
|
||||
<img id="live-image" class="camera" src="/stream.mjpg" alt="Livebild der Kamera">
|
||||
<canvas id="detection-overlay" width="640" height="480" aria-label="Linie und Auslösebereich einstellen"></canvas>
|
||||
</div>
|
||||
<h2 style="margin-top:1rem">Automatische Auslösung</h2>
|
||||
<details style="margin-top:1rem"><summary>Automatische Auslösung einstellen</summary>
|
||||
<p>Endpunkte der orangefarbenen Linie und Ecken des grünen Bereichs ziehen. Der Pfeil zeigt zum Grab. Änderungen werden erst nach dem Speichern aktiv.</p>
|
||||
<label for="detection-method">Erkennung wählen</label>
|
||||
<select id="detection-method">
|
||||
@@ -67,6 +100,7 @@
|
||||
<button id="reverse-direction">Richtung umkehren</button>
|
||||
<button id="save-detection">Linie und Bereich speichern</button>
|
||||
</div>
|
||||
</details>
|
||||
<p id="detection-status" class="muted" role="status">Erkennung lädt …</p>
|
||||
<p id="motion-details" class="muted" role="status" hidden></p>
|
||||
<p class="muted" id="camera-status">Kamera wird gestartet …</p>
|
||||
@@ -91,89 +125,34 @@
|
||||
</select>
|
||||
<button id="save-mode">Übernehmen</button>
|
||||
</div>
|
||||
<p class="notice"><strong>Grabablauf nur simuliert.</strong> Hand, Atemmotor, Licht und Ton bleiben simuliert. Kameraservos können im separaten Farbtest wirklich bewegt werden. Die automatische Erkennung kann ausschließlich diesen simulierten Ablauf auslösen.</p>
|
||||
<p class="notice"><strong>Grabablauf nur simuliert.</strong> Hand, Atemmotor, Licht und Ton bleiben simuliert. Kameraservos lassen sich auf der Seite „Kamera / Finger“ bedienen. Die automatische Erkennung kann ausschließlich diesen simulierten Ablauf auslösen.</p>
|
||||
<p class="muted">Die folgenden Knöpfe sind für manuelle Tests. Die Automatik gibt sich selbst frei.</p>
|
||||
<div class="row">
|
||||
<button id="zone-clear">Bereich frei (manueller Test)</button>
|
||||
<button id="test-trigger">Manuelle Testauslösung</button>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 1rem">
|
||||
<button class="danger" id="stop">Stopp</button>
|
||||
<button id="reset">Nach Stopp zurücksetzen</button>
|
||||
<button class="danger" id="stop">Grab stoppen</button>
|
||||
<button id="reset">Grab zurücksetzen</button>
|
||||
</div>
|
||||
<p id="message" role="status" aria-live="polite"></p>
|
||||
</section>
|
||||
<section class="events" id="follow-panel">
|
||||
<h2>Kamera folgt einem Farbpunkt · Testmodus</h2>
|
||||
<p>Halte einen kräftig roten oder grünen Aufkleber vor die Kamera. Ein Punkt am Finger geht auch. Kein Training und kein Internet nötig. Zuerst die automatische Auslösung ausschalten und speichern.</p>
|
||||
<div class="row">
|
||||
<button id="follow-green">Grün wählen</button><button id="follow-red">Rot wählen</button>
|
||||
<button id="follow-capture">Beispielfoto aufnehmen</button>
|
||||
</div>
|
||||
<p>Eigene Farbe: Beispielfoto aufnehmen und dort auf die Mitte des Farbpunkts tippen. Das Foto wird nicht gespeichert.</p>
|
||||
<canvas id="follow-canvas" width="640" height="480" style="width:100%;max-width:720px;background:#050808;cursor:crosshair" aria-label="Farbpunkt im Beispielfoto auswählen"></canvas>
|
||||
<p id="follow-status" role="status" aria-live="polite">Farbtest lädt …</p>
|
||||
<details>
|
||||
<summary>Servogrenzen und Richtung einstellen</summary>
|
||||
<p>Einheit µs = Pulsbreite, keine Winkel. Der Start setzt beide Servos auf 1500 µs; dabei kann die Halterung springen. Beim ersten Test Servohörner/Gestänge lösen, Mittelstellung prüfen und danach spannungsfrei wieder montieren. Grenzen nur schrittweise erweitern.</p>
|
||||
<div class="row">
|
||||
<label>PAN Minimum <input id="follow-pan-min" type="number" min="1100" max="1500" step="10" value="1400" style="width:7em"></label>
|
||||
<label>PAN Maximum <input id="follow-pan-max" type="number" min="1500" max="1900" step="10" value="1600" style="width:7em"></label>
|
||||
<label><input id="follow-pan-reverse" type="checkbox"> PAN umkehren</label>
|
||||
</div>
|
||||
<div class="row" style="margin-top:.6rem">
|
||||
<label>TILT Minimum <input id="follow-tilt-min" type="number" min="1100" max="1500" step="10" value="1400" style="width:7em"></label>
|
||||
<label>TILT Maximum <input id="follow-tilt-max" type="number" min="1500" max="1900" step="10" value="1600" style="width:7em"></label>
|
||||
<label><input id="follow-tilt-reverse" type="checkbox"> TILT umkehren</label>
|
||||
</div>
|
||||
<label>Farbtoleranz <input id="follow-tolerance" type="number" min="5" max="35" value="18" style="width:6em"></label>
|
||||
<button id="follow-save">Einstellungen speichern</button>
|
||||
</details>
|
||||
<p><label><input id="follow-ack" type="checkbox"> Mittelstellung und Bewegungsraum geprüft, Kamerakabel hat Spiel; Servoversorgung kann ich sofort abziehen.</label></p>
|
||||
<div class="row">
|
||||
<button id="follow-preview">Nur Farbvorschau starten</button>
|
||||
<button id="follow-hardware">Echte Kameranachführung starten</button>
|
||||
<button id="follow-stop" class="danger">Kameraservos stoppen</button>
|
||||
</div>
|
||||
<div style="border:2px solid #759c8f;border-radius:8px;padding:1rem;margin-top:1rem">
|
||||
<h2>Endpunkte einrichten · manuell</h2>
|
||||
<p>Langsam annähern, vor dem Anschlag loslassen und die Grenze speichern. Auch links/rechts nicht gegen den Anschlag drücken. Keine automatische Endpunkterkennung.</p>
|
||||
<button id="setup-start">Einrichtmodus starten</button>
|
||||
<p id="setup-note" role="status">Noch keine Grenze in dieser Sitzung gespeichert.</p>
|
||||
<p id="setup-limits" class="muted"></p>
|
||||
<p id="setup-position" class="value" role="status">Sollposition: PAN 1500 / TILT 1500 µs</p>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,minmax(80px,150px));gap:.7rem;max-width:480px">
|
||||
<span></span><button id="setup-up" style="font-size:1.3rem;touch-action:none;user-select:none">▲ Hoch halten</button><span></span>
|
||||
<button id="setup-left" style="font-size:1.3rem;touch-action:none;user-select:none">◀ Links halten</button>
|
||||
<button id="setup-release" class="danger">Bewegung anhalten</button>
|
||||
<button id="setup-right" style="font-size:1.3rem;touch-action:none;user-select:none">Rechts halten ▶</button>
|
||||
<span></span><button id="setup-down" style="font-size:1.3rem;touch-action:none;user-select:none">▼ Runter halten</button><span></span>
|
||||
</div>
|
||||
<p>Taste gedrückt halten: langsame Bewegung. Loslassen: Zielposition bleibt stehen. Bei fehlendem Tastensignal endet die Bewegung spätestens nach 0,35 s, sofern Pi und Treiber reagieren.</p>
|
||||
<div class="row">
|
||||
<button id="setup-save-left">Links-Grenze speichern</button>
|
||||
<button id="setup-save-right">Rechts-Grenze speichern</button>
|
||||
<button id="setup-save-up">Oben-Grenze speichern</button>
|
||||
<button id="setup-save-down">Unten-Grenze speichern</button>
|
||||
</div>
|
||||
<p class="muted">Speichert den angeforderten Pulswert mit 10 µs Abstand nach innen, keine gemessene Position. Beim Einrichten gelten 1100–1900 µs, auch außerhalb der bisherigen Farbtest-Grenzen. Falls das nicht reicht: stoppen, nichts erzwingen. Zum Beenden „Kameraservos stoppen“ drücken. Neue Sitzung startet wieder bei 1500 µs.</p>
|
||||
</div>
|
||||
<p class="notice">Nur Kameraservos CH0/CH1. Kein automatischer Start nach Neustart. Im Farbtest: Bei Punktverlust keine Suchbewegung; nach einer Sekunde wird die Servositzung beendet. Bei verlorener Bedienverbindung oder Kamerafehler werden die PWM-Signale abgeschaltet, soweit die Verbindung zum Treiber funktioniert. Die Stromversorgung bleibt eingeschaltet. Bei einem Hardwarefehler Servoversorgung abziehen.</p>
|
||||
</section>
|
||||
<section class="events">
|
||||
<h2>Gespeicherte Ereignisse</h2>
|
||||
<details><summary>Gespeicherte Ereignisse</summary>
|
||||
<p class="muted">Erfolgreiche manuelle und automatische Auslösungen bleiben nach einem Neustart erhalten. Eine Profilzuordnung kann hier zum Testen oder Korrigieren gesetzt werden.</p>
|
||||
<button id="refresh-events">Ereignisse aktualisieren</button>
|
||||
<ul id="events-list"><li>Lädt …</li></ul>
|
||||
</details>
|
||||
</section>
|
||||
<section class="events">
|
||||
<h2>Personenprofile</h2>
|
||||
<details><summary>Personenprofile</summary>
|
||||
<p class="notice">Die automatische Wiedererkennung per Kamera ist noch nicht aktiv. Diese Profile sind derzeit nur manuell angelegte Kennungen; Bilder oder Gesichtsmerkmale werden nicht gespeichert.</p>
|
||||
<div class="row">
|
||||
<input id="profile-label" type="text" maxlength="80" aria-label="Profilkennung" placeholder="Profilkennung oder Spitzname">
|
||||
<button id="create-profile">Profil anlegen</button>
|
||||
</div>
|
||||
<ul id="profiles-list"><li>Lädt …</li></ul>
|
||||
</details>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
@@ -330,8 +309,8 @@
|
||||
$('save-mode').onclick = () => action('/api/mode', { mode: $('mode').value });
|
||||
$('zone-clear').onclick = () => action('/api/zone-clear');
|
||||
$('test-trigger').onclick = () => action('/api/test-trigger');
|
||||
$('stop').onclick = () => action('/api/stop');
|
||||
$('reset').onclick = () => action('/api/reset');
|
||||
$('stop').onclick = () => action('/api/grave/stop');
|
||||
$('reset').onclick = () => action('/api/grave/reset');
|
||||
$('create-profile').onclick = async () => {
|
||||
try {
|
||||
await api('/api/profiles', { label: $('profile-label').value });
|
||||
@@ -460,170 +439,13 @@
|
||||
detectionConfig = c; $('detection-enabled').checked = c.enabled;
|
||||
$('detection-method').value = c.method; updateDetectionHelp(); drawDetection();
|
||||
}).catch(error => { $('detection-status').textContent = error.message; });
|
||||
// Camera marker test has its own frame/overlay, never the trigger geometry.
|
||||
let followConfig = null, followSession = null, followSample = null, followBusy = false;
|
||||
let followEpoch = 0;
|
||||
const followCanvas = $('follow-canvas'), followCtx = followCanvas.getContext('2d');
|
||||
function fillFollow(c) {
|
||||
followConfig = c;
|
||||
for (const axis of ['pan','tilt']) {
|
||||
$( `follow-${axis}-min`).value = c[axis+'_min'];
|
||||
$( `follow-${axis}-max`).value = c[axis+'_max'];
|
||||
$( `follow-${axis}-reverse`).checked = c[axis+'_reverse'];
|
||||
}
|
||||
$('follow-tolerance').value = c.tolerance;
|
||||
}
|
||||
async function showFollowImage(b64, target, epoch) {
|
||||
const image = new Image(); image.src = 'data:image/jpeg;base64,'+b64;
|
||||
await image.decode();
|
||||
if (epoch !== followEpoch) return;
|
||||
followCtx.drawImage(image,0,0,640,480);
|
||||
followCtx.strokeStyle = '#fff'; followCtx.lineWidth = 2;
|
||||
followCtx.beginPath(); followCtx.moveTo(305,240); followCtx.lineTo(335,240);
|
||||
followCtx.moveTo(320,225); followCtx.lineTo(320,255); followCtx.stroke();
|
||||
if (target) {
|
||||
followCtx.strokeStyle = '#ffca78'; followCtx.lineWidth = 5;
|
||||
followCtx.beginPath(); followCtx.arc(target.x*640,target.y*480,Math.max(15,target.radius*640),0,Math.PI*2); followCtx.stroke();
|
||||
}
|
||||
}
|
||||
async function followAction(fn) {
|
||||
if (followBusy) return;
|
||||
followBusy = true;
|
||||
try { await fn(); }
|
||||
catch (e) { $('follow-status').textContent = e.message; }
|
||||
finally { followBusy = false; }
|
||||
}
|
||||
async function saveFollow() {
|
||||
const c = {...followConfig};
|
||||
for (const axis of ['pan','tilt']) {
|
||||
c[axis+'_min'] = Number($(`follow-${axis}-min`).value);
|
||||
c[axis+'_max'] = Number($(`follow-${axis}-max`).value);
|
||||
c[axis+'_reverse'] = $(`follow-${axis}-reverse`).checked;
|
||||
}
|
||||
c.tolerance = Number($('follow-tolerance').value);
|
||||
fillFollow(await api('/api/follow/config',c));
|
||||
}
|
||||
for (const [id,hue] of [['follow-green',85],['follow-red',0]]) {
|
||||
$(id).onclick = () => followAction(async () => {
|
||||
if (!followConfig) throw new Error('Einstellungen noch nicht geladen');
|
||||
fillFollow(await api('/api/follow/config',{...followConfig,hue}));
|
||||
$('follow-status').textContent = 'Farbe gespeichert. Vorschau starten.';
|
||||
});
|
||||
}
|
||||
$('follow-save').onclick = () => followAction(async () => {
|
||||
await saveFollow(); $('follow-status').textContent = 'Grenzen und Richtung gespeichert';
|
||||
});
|
||||
$('follow-capture').onclick = () => followAction(async () => {
|
||||
const photo = await api('/api/follow/capture');
|
||||
followSample = photo.token; followEpoch++;
|
||||
await showFollowImage(photo.image_base64,null,followEpoch);
|
||||
$('follow-status').textContent = 'Standbild: Jetzt auf die Mitte des Farbpunkts tippen.';
|
||||
});
|
||||
followCanvas.onclick = e => followAction(async () => {
|
||||
if (!followSample) return;
|
||||
const rect = followCanvas.getBoundingClientRect();
|
||||
const config = await api('/api/follow/select',{token:followSample,
|
||||
x:(e.clientX-rect.left)/rect.width,y:(e.clientY-rect.top)/rect.height});
|
||||
fillFollow(config); $('follow-status').textContent = 'Eigene Farbe übernommen. Vorschau starten.';
|
||||
});
|
||||
async function beginFollow(hardware, operation = 'follow') {
|
||||
if (hardware && !$('follow-ack').checked) throw new Error('Bitte zuerst Mittelstellung und freien Bewegungsraum prüfen und bestätigen.');
|
||||
await saveFollow();
|
||||
const result = await api('/api/follow/start',{hardware,acknowledged:$('follow-ack').checked,operation});
|
||||
releaseJog(); jogSequence = 0; followSession = result.session; followSample = null; followEpoch++;
|
||||
$('follow-status').textContent = hardware ? 'Echte Servos aktiv' : 'Farbvorschau ohne Servobewegung';
|
||||
}
|
||||
$('follow-preview').onclick = () => followAction(() => beginFollow(false));
|
||||
$('follow-hardware').onclick = () => followAction(() => beginFollow(true));
|
||||
$('follow-stop').onclick = async () => {
|
||||
releaseJog(); setupReady = false; followSession = null; followEpoch++;
|
||||
try { await api('/api/follow/stop'); $('follow-status').textContent = 'Kameraservos gestoppt'; }
|
||||
catch(e) { $('follow-status').textContent = 'Stopp nicht bestätigt – Servoversorgung abziehen. '+e.message; }
|
||||
};
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden && followSession) $('follow-stop').click();
|
||||
});
|
||||
// Hold-to-jog: separate short motion lease; sequence numbers reject late requests.
|
||||
let jogTimer = null, jogSequence = 0, setupReady = false, followDemo = false;
|
||||
async function sendJog(direction) {
|
||||
if (!followSession) return;
|
||||
return api('/api/follow/jog',{session:followSession,direction,sequence:++jogSequence});
|
||||
}
|
||||
function releaseJog() {
|
||||
if (jogTimer === null) return;
|
||||
clearInterval(jogTimer); jogTimer = null;
|
||||
sendJog(null).catch(e => { $('follow-status').textContent = 'Tastenstopp nicht bestätigt: '+e.message; });
|
||||
}
|
||||
function pressJog(direction) {
|
||||
if (!setupReady || !followSession || jogTimer !== null) return;
|
||||
const send = () => sendJog(direction).catch(e => {
|
||||
releaseJog(); $('follow-status').textContent = e.message;
|
||||
});
|
||||
jogTimer = setInterval(send,100); send();
|
||||
}
|
||||
for (const direction of ['left','right','up','down']) {
|
||||
const button = $('setup-'+direction);
|
||||
button.disabled = true;
|
||||
button.onpointerdown = e => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault(); button.setPointerCapture(e.pointerId); pressJog(direction);
|
||||
};
|
||||
button.onpointerup = button.onpointercancel = button.onlostpointercapture = releaseJog;
|
||||
button.onkeydown = e => {
|
||||
if ([' ','Enter'].includes(e.key)) { e.preventDefault(); if (!e.repeat) pressJog(direction); }
|
||||
};
|
||||
button.onkeyup = e => { if ([' ','Enter'].includes(e.key)) { e.preventDefault(); releaseJog(); } };
|
||||
button.onblur = releaseJog;
|
||||
$('setup-save-'+direction).disabled = true;
|
||||
$('setup-save-'+direction).onclick = () => followAction(async () => {
|
||||
releaseJog();
|
||||
await sendJog(null);
|
||||
fillFollow(await api('/api/follow/remember',{session:followSession,direction}));
|
||||
$('setup-note').textContent = 'Grenze gespeichert – mit 10 µs Abstand nach innen.';
|
||||
});
|
||||
}
|
||||
$('setup-release').onclick = releaseJog;
|
||||
document.addEventListener('pointerup',releaseJog);
|
||||
document.addEventListener('pointercancel',releaseJog);
|
||||
window.addEventListener('blur',releaseJog);
|
||||
$('setup-start').onclick = () => followAction(() => beginFollow(!followDemo,'setup'));
|
||||
|
||||
async function pollFollow() {
|
||||
$('all-stop').onclick = async () => {
|
||||
try {
|
||||
if (followSession) await api('/api/follow/heartbeat',{session:followSession});
|
||||
const r = await fetch('/api/follow',{cache:'no-store'});
|
||||
if (!r.ok) throw new Error('Farbtest nicht erreichbar');
|
||||
const f = await r.json();
|
||||
followDemo = f.demo;
|
||||
$('setup-start').textContent = followDemo ? 'Einrichtmodus als Demo starten' : 'Einrichtmodus starten';
|
||||
setupReady = f.active && f.operation === 'setup' && !!followSession;
|
||||
if (!setupReady) releaseJog();
|
||||
for (const direction of ['left','right','up','down']) {
|
||||
$('setup-'+direction).disabled = !setupReady;
|
||||
$('setup-save-'+direction).disabled = !setupReady;
|
||||
}
|
||||
$('setup-limits').textContent = 'Gespeicherte Grenzen: PAN '+f.config.pan_min+'–'+f.config.pan_max+' / TILT '+f.config.tilt_min+'–'+f.config.tilt_max+' µs';
|
||||
$('setup-position').textContent = 'Sollposition: PAN '+f.pan_us+' / TILT '+f.tilt_us+' µs';
|
||||
if (!followConfig) {
|
||||
fillFollow(f.config);
|
||||
$('follow-status').textContent = 'Bereit. Farbe wählen oder Beispielfoto aufnehmen.';
|
||||
}
|
||||
if (!f.active) followSession = null;
|
||||
if (f.active || f.error) $('follow-status').textContent = f.error ||
|
||||
(f.operation === 'setup' ? (f.hardware ? 'Einrichtmodus · echte Servos' : 'Einrichtmodus · nur Demo')
|
||||
: (f.hardware ? 'Echte Servos aktiv' : 'Vorschau ohne Bewegung'))+' · '+
|
||||
(f.operation === 'setup' ? 'Richtungstaste gedrückt halten' : (f.target ? 'Punkt erkannt' : 'Kein eindeutiger Farbpunkt'))+' · PAN '+f.pan_us+' / TILT '+f.tilt_us+' µs';
|
||||
if (f.active && f.image_base64 && !followSample) await showFollowImage(f.image_base64,f.target,followEpoch);
|
||||
} catch(e) {
|
||||
if (followSession) {
|
||||
releaseJog(); setupReady = false; followSession = null;
|
||||
$('follow-status').textContent = 'Verbindung verloren; Nachführung wird beendet. '+e.message;
|
||||
}
|
||||
}
|
||||
setTimeout(pollFollow,300);
|
||||
}
|
||||
pollFollow();
|
||||
|
||||
await api('/api/stop');
|
||||
$('all-stop-status').textContent = 'Grab gestoppt und Kameraservo-PWM beendet.';
|
||||
} catch(e) { $('all-stop-status').textContent = 'Stopp nicht bestätigt: '+e.message; }
|
||||
};
|
||||
refreshDetection();
|
||||
setInterval(drawDetection, 250);
|
||||
refresh();
|
||||
|
||||
Reference in new issue
Block a user