Draft Focus
This commit is contained in:
205
doc/Focus.md
Normal file
205
doc/Focus.md
Normal file
@@ -0,0 +1,205 @@
|
||||
## 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. 0–255, 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 (~1–2h) |
|
||||
| `src/focusService.js` (Router + Jest) | klein (~1h) |
|
||||
| `server.js`-Integration (Probe beim Start, Apply persistierter Werte) | klein |
|
||||
| `config.html`/`config.js`-UI-Erweiterung | klein–mittel (~1–2h) |
|
||||
| **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).
|
||||
Reference in New Issue
Block a user