added point mode

This commit is contained in:
marcinator_2 committed 2026-10-15 20:31:02 +02:00
1 parent 044ad2afbe
commit fce8f62534
16 files changed
+1547 -10

No files matched your search

+222 -1
View File
@@ -91,7 +91,7 @@
</select>
<button id="save-mode">Übernehmen</button>
</div>
<p class="notice"><strong>Aktoren nur simuliert.</strong> Kein Motor, Licht oder Ton ist mit dieser Version verbunden. 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 können im separaten Farbtest wirklich bewegt werden. 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>
@@ -103,6 +103,63 @@
</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>
<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>
@@ -403,6 +460,170 @@
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() {
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();
refreshDetection();
setInterval(drawDetection, 250);
refresh();