Files
appRobotWebcam/doc/Focus.md
2026-06-16 20:59:30 +02:00

10 KiB
Raw Blame History

Entscheidungsgrundlage Fokus-Steuerung (C920/C922)

Status: Nicht umgesetzt. Reine Entscheidungsgrundlage, um abzuwägen ob/wie sich das lohnt, bevor Aufwand investiert wird.

Betrifft nur cam2 (laut cameras.json aktuell als C920 dokumentiert, laut Nutzer real eine C922 beide haben denselben UVC-Autofokus-Motor, der Ansatz unten gilt für beide identisch). cam0/cam1 (C270) haben keinen Fokusmotor Fixed-Focus-Linse, keine UVC-Focus-Controls vorhanden. Jeder UI-/API-Entwurf muss das abbilden (Capability-Check, nicht Kamera-ID-Whitelist falls mal eine C270 gegen eine weitere C920/C922 ausgetauscht wird, soll es automatisch funktionieren).


Kontext: Was steuert man da eigentlich?

Fokus ist ein UVC-Geräte-Control, kein FFmpeg-Stream-Parameter. Er liegt auf derselben Ebene wie Belichtung/Weißabgleich gesetzt per ioctl auf /dev/videoN, unabhängig vom Capture-Pfad. Zwei relevante Controls (Linux-Namen, via v4l2-ctl --list-ctrls abfragbar):

Control Bedeutung
focus_automatic_continuous Autofokus an (1) / aus (0)
focus_absolute manueller Fokuswert, Bereich/Step kameraabhängig (C922 typ. 0255, Step 5)

focus_absolute greift nur, wenn focus_automatic_continuous=0 ist sonst überschreibt der Autofokus-Regler den Wert laufend wieder.

Wichtiger Unterschied zu allem bisher im Projekt: Bisher hat ausschließlich CameraSwitch das Gerät geöffnet (FFmpeg, exklusiv gedacht laut Architektur-Doku). Eine Fokus-Steuerung braucht einen zweiten, kurzlebigen Öffner desselben Devices, während FFmpeg ggf. gerade live streamt. Ob das auf dieser Hardware klappt, ohne den Live-Stream zu stören, ist unklar und zwingend zuerst auf dem Host zu prüfen (Memory-Regel: auf Host messen statt vorhersagen).


Werkzeug-Wahl: v4l2-ctl per spawn, kein neues npm-Package

Konsistent mit dem Rest des Projekts (FFmpeg wird ebenfalls per child_process.spawn aufgerufen, package.json hat bewusst nur express als Dependency):

v4l2-ctl -d /dev/video4 --list-ctrls                       # Capability-Check + Wertebereich
v4l2-ctl -d /dev/video4 --set-ctrl=focus_automatic_continuous=0
v4l2-ctl -d /dev/video4 --set-ctrl=focus_absolute=30

v4l2-ctl ist im Image bereits vorhanden (laut Architektur-Doku, dort bisher nur diagnostisch genutzt). Keine zusätzliche Library, kein zusätzliches Docker-Image-Gewicht. Alternative wäre eine native Node-V4L2-Bindung (z. B. v4l2-ctrls-artige Pakete) unnötig, da v4l2-ctl exakt das tut und schon im Container ist.


Geplanter Aufbau

Neues Modul src/focusControl.js

Trennung reine Logik (Jest-testbar, kein Hardware-Zugriff) von Ausführung, exakt das Muster aus src/configService.js:

// Reine Logik  kein spawn, kein fs:
function parseListCtrls(stdout) { /* → { focus_automatic_continuous: {min,max,default}, focus_absolute: {min,max,step,default} } | {} wenn keine Focus-Controls */ }
function buildSetArgs(device, { auto, value }) { /* → Array von v4l2-ctl-Aufrufen/Args, auto IMMER vor value gesetzt */ }
function clampFocus(value, caps) { /* auf min/max/step runden */ }

// Ausführung  spawn, Promise-basiert (analog _captureAt-Stil in cameraSwitch.js):
async function probeFocusCaps(device) { /* spawn v4l2-ctl --list-ctrls, parseListCtrls */ }
async function applyFocus(device, { auto, value }) { /* spawn v4l2-ctl --set-ctrl=..., sequenziell */ }

probeFocusCaps liefert {} (kein Fehler) für Geräte ohne Focus-Control → das ist der Capability-Check, der C270 automatisch ausblendet, ohne Kamera-Modell-Strings zu vergleichen.

Wann proben?

Beim Server-Start, einmal pro Kamera, parallel zu den CameraSwitch-Instanzen (analog loadCalibrations() in server.js). Ergebnis in camsMeta[i].focusCaps ablegen. Kein Re-Probe zur Laufzeit nötig (Hardware-Capabilities ändern sich nicht).

Offene Frage, auf dem Host zu klären: Funktioniert --list-ctrls/--set-ctrl zuverlässig, während CameraSwitch für dieselbe Kamera gerade per FFmpeg streamt (state === 'live')? Falls nicht (z. B. VIDIOC_S_CTRL: Device or resource busy):

  • Fallback A: Fokus-Änderungen kurz den Live-Stream pausieren (_killCurrentAndWait + _spawnLive, exakt der bestehende HD-Grab-Mechanismus, nur ohne Auflösungswechsel). Kostet ein kurzes Stream-Einfrieren (wie bei jedem reconfigure()/HD-Grab schon heute).
  • Fallback B: Falls selbst das nicht reicht (Gerät wirklich exklusiv), müsste Fokus-Setzen als neue CameraSwitch-Methode laufen, die den Lock nutzt statt eines externen Prozesses.

→ Das ist der einzige echte Unsicherheitsfaktor in diesem Plan. Vor Implementierung mit einem 5-Minuten-Test auf dem Host klären (v4l2-ctl --set-ctrl=focus_absolute=30 während der Viewer offen ist und cam2 streamt).

Persistenz: cameras.json erweitern

Analog zu liveSize/stream (siehe 12_cameraConfig_roadmap.md) neue optionale Felder, nur bei Kameras mit Fokus-Support gesetzt:

{
  "id": "cam2",
  "...": "...",
  "focusAuto": false,
  "focusAbsolute": 30
}

Fehlen die Felder (wie aktuell bei allen drei Kameras) → Kamera bleibt im Werks-/Treiber-Default (meist focus_automatic_continuous=1). Beim Server-Start wird ein persistierter Wert einmalig angewendet (applyFocus() direkt nach probeFocusCaps(), bevor CameraSwitch.start() o. On-Demand-Live anläuft) dann ist der Fokus von Anfang an korrekt, kein Nachjustieren beim ersten Frame nötig.

API: src/focusService.js (neuer Router, Muster wie configService.js)

GET  /api/focus
  → { cameras: [
        { id:"cam0", supported:false },
        { id:"cam1", supported:false },
        { id:"cam2", supported:true, auto:false, value:30, min:0, max:255, step:5 }
     ] }

POST /api/focus/:id          Body: { auto: boolean, value?: number }
  → validiert (id bekannt, supported, value im Bereich) → v4l2-ctl ausführen
    (auto zuerst, dann value falls auto:false) → persistiert in cameras.json
    (gleiches atomares tmp+rename-Muster wie configService) → 200 mit Ist-Zustand
  → 400 bei unbekannter id / nicht unterstützt / Wert außerhalb Bereich
  → 409 falls Gerät kurzfristig busy (siehe offene Frage oben)  kein Server-500,
    Client kann anzeigen „bitte erneut versuchen"

UI: Erweiterung von config.html (kein neues Formular)

Pro Kamera-Zeile, nur wenn supported:true, zusätzliche Spalte:

┌──────────┬──────────────┬────────────────┬───────────────────────────┐
│ cam2     │ Kamera 2     │ [320×240    ▼] │ Fokus: ☑ Auto  [────●────] │
└──────────┴──────────────┴────────────────┴───────────────────────────┘
  • Checkbox „Auto" (focus_automatic_continuous) an = Slider deaktiviert/ausgegraut.
  • Slider (focus_absolute, min/max/step aus GET /api/focus) nur aktiv wenn Auto aus. Live-Vorschau (Slider-Drag → Debounce ~300 ms → POST /api/focus/:id), kein separater „Anwenden"-Button nötig, da Fokus anders als Auflösung keinen Stream-Restart auslösen sollte (siehe offene Frage falls Fallback A nötig wird, kurzes Einfrieren pro Drag-Schritt einplanen, dann doch debounce auf ~800 ms hochsetzen).
  • Für cam0/cam1: Spalte entweder ausgeblendet oder Platzhaltertext „kein Autofokus (C270)".

Test-Strategie

Jest (ohne Hardware), analog configValidate.test.js/configMerge.test.js:

  • parseListCtrls: echte v4l2-ctl --list-ctrls-Textbeispiele (mit und ohne Focus-Controls) → korrektes Capability-Objekt bzw. {}.
  • clampFocus: Werte außerhalb Bereich, Werte nicht auf step-Raster.
  • buildSetArgs: Reihenfolge garantiert auto-vor-value; auto:true setzt KEIN focus_absolute mit.
  • Validierung im Router: unbekannte id, supported:false, Wert außerhalb Bereich.

Host-Test (zwingend vor jeder UI-Arbeit):

  1. v4l2-ctl -d /dev/video4 --list-ctrls → Bestätigen, dass C922 (cam2) Focus-Controls meldet und C270s (cam0/cam1) keine.
  2. Die offene Frage von oben: --set-ctrl während cam2 live streamt testen.
  3. Sichtprüfung: Schärfentest-Chart oder Textseite vor die Kamera halten, focus_absolute über den gesamten Bereich durchfahren, im Viewer beobachten ob sich die Schärfe ändert (bestätigt, dass die Werte überhaupt etwas bewirken manche UVC-Geräte melden Controls, die der Treiber dann ignoriert).

Abgrenzung (bewusst NICHT Teil dieses Plans)

  • C270: keine Fokus-Funktion Hardware kann es nicht, kein Workaround sinnvoll (digitales Nachschärfen ist kein echter Fokus, nur ein Filter auf bereits unscharfem Bild).
  • Spot-/Touch-Autofokus (auf einen Bildbereich tippen): UVC kennt das nicht, das ist proprietäre Logitech-Software-Logik (Logi Capture u. ä.), nicht über v4l2 erreichbar.
  • Automatisches Nachfokussieren bei Auflösungswechsel: Fokus ist ein reiner Geräte-/Linsenzustand, unabhängig von liveSize/hiresSize ändert sich durch reconfigure() oder HD-Grab nicht, daher keine Interaktion mit dem bestehenden Live/Grab-State-Machine-Code nötig (außer ggf. Fallback A oben).
  • Authentifizierung auf dem neuen Endpoint: gleiche Linie wie /api/config erst bei geplantem Internet-Zugang.

Aufwandsschätzung (grob, zur Priorisierung)

Teil Aufwand
Host-Test der offenen Frage (busy-Verhalten) 15 Min
src/focusControl.js (Logik + Jest) klein (~12h)
src/focusService.js (Router + Jest) klein (~1h)
server.js-Integration (Probe beim Start, Apply persistierter Werte) klein
config.html/config.js-UI-Erweiterung kleinmittel (~12h)
Gesamt ein halber bis ein Tag, abhängig vom Ergebnis des Host-Tests

Empfehlung: Lohnt sich kleiner, klar abgegrenzter Scope, nutzt bestehende Muster 1:1 (configService als Vorlage). Einziges Risiko ist der Busy-Test; der ist in 15 Minuten geklärt und entscheidet nur zwischen Plan A (einfach) und Fallback A (geringfügig komplexer, aber bereits vorhandener Mechanismus).