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

206 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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):
```bash
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`:
```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:
```json
{
"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).