Decision Log
Architektur- und Design-Entscheidungen des Rust-Rewrites. Jede Entscheidung nennt Begründung und Widerspruch (wie ein Mensch die Entscheidung umkehren/anpassen kann). Neueste Einträge oben.
Format: YYYY-MM-DD — getroffen von Claude (Opus 4.8), sofern nicht anders vermerkt.
2026-06-13 — ONNX-Runtime-Library beschafft (NICHT über AUR) + Config-Robustheit
Kontext: ort mit load-dynamic braucht libonnxruntime.so zur Laufzeit; sie fehlte. Der Nutzer warnte vor dem laufenden AUR-Supply-Chain-Angriff „Atomic Arch" (seit 2026-06-11, von Sonatype entdeckt): übernommene/verwaiste AUR-Pakete mit manipulierten PKGBUILDs laden beim Build via yay/paru bösartige npm-Pakete (atomic-lockfile, js-digest) → Credential-Stealer + eBPF-Rootkit, gezielt auf Dev-Workstations. Quellen: Sonatype (https://www.sonatype.com/blog/atomic-arch-npm-campaign-adds-malicious-dependency), The Hacker News (https://thehackernews.com/2026/06/over-400-arch-linux-aur-packages.html), BleepingComputer (https://www.bleepingcomputer.com/news/security/over-400-arch-linux-packages-compromised-to-push-rootkit-infostealer/).
Entscheidung: AUR komplett vermieden. onnxruntime ist auch nicht in den offiziellen (signierten) Arch-Repos. Daher direkt das offizielle Microsoft-GitHub-Release geladen: onnxruntime-linux-x64-1.23.2.tgz. Eine vorgefertigte Binary OHNE PKGBUILD/npm-preinstall — der Atomic-Arch-Vektor greift hier also nicht.
Versionswahl 1.23.2: ort 2.0.0-rc.11 → ONNX Runtime 1.23.2; rc.12 ist neuer (1.23.x). Die ONNX-Runtime-C-API ist abwärtskompatibel, d.h. 1.23.2 deckt jede von ort angeforderte API-Version ab. Quelle: pykeio/ort Releases.
Validierung (vor Nutzung):
-
Quelle:
github.com/microsoft/onnxruntimeRelease über HTTPS (offizielle Org). -
sha256 des Downloads:
1fa4dcaef22f6f7d5cd81b28c2800414350c10116f5fdd46a2160082551c5f9b. -
ELF 64-bit x86-64 shared object; exportiertes Symbol
OrtGetApiBase@@VERS_1.23.2vorhanden. -
End-to-End-Test: App mit
ORT_DYLIB_PATH=runtime/libonnxruntime.so.1.23.2gestartet → LogONNX-Modell geladen model=models/yolov8n-pose.onnx(gesamte Kette ort→.so→Modell ok).
Ablage: runtime/libonnxruntime.so* (gitignored, arch-spezifisch x86_64). Start mit ORT_DYLIB_PATH=$PWD/runtime/libonnxruntime.so.1.23.2.
Begleitfix: Config.cameras bekam #[serde(default)] — eine frische Config ohne lädt jetzt (Kameras werden per Dashboard ergänzt). Vorher: harter Parse-Fehler.
Widerspruch / Anpassung:
-
Für den Pi 5 (aarch64) die passende
onnxruntime-linux-aarch64-1.23.2.tgzverwenden (die x86_64-.sofunktioniert dort NICHT). -
Wer kein manuelles
.so-Handling will: inCargo.tomlort-Featureload-dynamicdurchdownload-binariesersetzen (ort lädt die passende Runtime beim Build automatisch). -
System-weite Installation gewünscht:
.sonach/usr/local/lib+ldconfig, dann istORT_DYLIB_PATHunnötig — aber bewusst NICHT gemacht (kein Eingriff ins System).
2026-06-13 — Pose-Modell models/yolov8n-pose.onnx erzeugt (nicht eingecheckt)
Herkunft (reproduzierbar): Offizielles Ultralytics-Gewicht yolov8n-pose.pt (GitHub ultralytics/assets release v8.4.0), exportiert nach ONNX mit ultralytics 8.4.66 in einer wegwerfbaren uv-venv (Python 3.12, CPU-torch 2.12). Export-Parameter: format=onnx, imgsz=640, opset=17, simplify=True, dynamic=False.
Verifiziert: Lädt in onnxruntime, Input images [1,3,640,640], Output output0 [1,56,8400] (= 4 bbox + 1 conf + 17·3 Keypoints) — exakt wie pipeline.rs erwartet. Dummy-Inferenz erfolgreich. sha256 der ONNX-Datei: c26f8e17a3965a0e47b359a6eb873fdef16d82f32f067fd2cbd8d3940a03604e.
Nicht in Git: .gitignore schließt *.onnx/models/ bewusst aus (13,5 MB Binary). Das Modell muss auf jedem Zielsystem separat bereitgestellt werden.
Begründung opset 17: universell von ONNX Runtime 1.18 (via ort-Feature api-18) unterstützt; vermeidet Inkompatibilitäten neuerer opsets.
Offener Laufzeit-Blocker (nicht Teil dieser Aufgabe): Das ort-Feature load-dynamic erfordert libonnxruntime.so zur Laufzeit (via ORT_DYLIB_PATH oder Systempfad). Auf dieser Maschine NICHT vorhanden → der App-Start würde die Session nicht laden können. Außerdem ist die .so architekturspezifisch (Dev = x86_64, Pi 5 = aarch64).
Widerspruch / Anpassung: Anderes Modell (z.B. yolov8s-pose für mehr Genauigkeit) analog exportieren und detection.model_path anpassen. Soll das Modell doch versioniert werden, .gitignore-Einträge entfernen und Git LFS verwenden.
2026-06-13 — ffmpeg-Eingabe quellenadaptiv (RTSP / V4L2 / Datei) für OBS-Tests
Kontext: Der Capture-Pfad hatte -rtsp_transport tcp fest verdrahtet. Der geplante Test mit OBS Virtual Cam liefert aber ein V4L2-Gerät (/dev/videoN), kein RTSP — damit wäre kein Test möglich gewesen.
Entscheidung: camera::ffmpeg_input_args(url) wählt die Eingabe nach Präfix: rtsp://→RTSP/TCP, /dev/video*→-f v4l2 (OBS), sonst→lokale Datei mit -stream_loop -1 (Testvideo läuft in Schleife). Geteilt von Capture- und Segment-Recorder (DRY), reine Funktion (unit-getestet).
Begründung: Ermöglicht den OBS-Test ohne Code-Änderung (Kamera-URL = /dev/video0) und sogar direktes Testen mit heruntergeladenen Sturz-Videos (URL = Dateipfad, loopt).
Unsicherheit / Caveat: Bei V4L2-Rohvideo schlägt die Segment-Aufzeichnung (-c copy → mpegts) fehl und loggt alle ~5 s eine Warnung; die Erkennung läuft davon unberührt. Für Datei-Quellen (mp4) funktioniert -c copy. OBS Virtual Cam liefert je nach Setup YUYV/MJPEG — ich konnte das exakte Pixelformat hier nicht verifizieren; ffmpeg skaliert/konvertiert im Capture-Pfad ohnehin nach rgb24, das sollte tragen.
Widerspruch / Anpassung: Wird die Datei-Schleife beim Testen nicht gewünscht, -stream_loop -1 im else-Zweig entfernen. Soll V4L2 auch aufgezeichnet werden, im Segment-Recorder -c copy durch einen Encoder (z.B. -c:v libx264) ersetzen.
2026-06-13 — Sturz-Dynamik + Per-Person-Tracking (detection/dynamics.rs)
Kontext: Der bisherige TemporalTracker erkannte nur einen Zustand ("7 von 10 Frames liegend") pro Kamera und konnte einen Sturz nicht von einer Person unterscheiden, die sich absichtlich hinlegt/hinsetzt, oder von einem knienden Helfer. Auch wurde bei mehreren Personen nur "irgendwer liegt" geodert (keine Identität).
Was als Sturz gilt — und wie ich es hergeleitet habe
Recherche (nicht aus dem Bauch): mehrere CV-Fall-Detection-Arbeiten beschreiben dasselbe Grundprinzip — „ein Sturz = liegende/gebeugte Pose UND Geschwindigkeit über einer Schwelle", und konkret: „fällt jemand schnell, ist es kein Vorsatz; fällt jemand langsam, legt er sich absichtlich hin". Genutzte Standard-Merkmale: Abstiegsgeschwindig- keit des Hüft-/Körperzentrums, Körperachsen-Winkel, bbox-Seitenverhältnis. Quellen:
-
MDPI Enhanced Fall Detection (YOLOv7-W6-Pose) — https://www.mdpi.com/1999-5903/16/12/472
-
arXiv Pose-Based Fall Detection on Standard CPUs — https://arxiv.org/pdf/2503.19501
-
ScienceDirect Vision-based Human Fall Detection: A Review — https://www.sciencedirect.com/science/article/pii/S1877050924017393/pdf
-
TDS Fall Detection using Pose Estimation — https://towardsdatascience.com/fall-detection-using-pose-estimation-a8f7fd77081d/
Definition (umgesetzt): Ein Sturz wird pro Person ausgelöst, wenn ALLE gelten:
-
Schneller Abstieg — Geschwindigkeit des bbox-Zentrums (normierte y/s, y nach unten) überschreitet
fall_velocity_thresholdinnerhalb vonvelocity_window_seconds. -
Liegende Pose —
classify_fall(Aspect-Ratio Signal A ODER Torso-Winkel Signal B). -
Aufprall-Persistenz — Pose bleibt ≥
min_ground_secondsliegend. -
der Abstieg lag ≤
descent_window_secondsvor der bestätigten Liege-Phase, -
und die Person ist nicht im
cooldown_seconds-Fenster.
So wird der Sturz (schnell zu Boden + bleibt liegen) vom absichtlichen Hinlegen (langsam) und von kurzem Bücken/Springen (keine Persistenz) getrennt.
Per-Person-Tracking
KISS-Tracker (SORT-artig, ohne Kalman/Hungarian): Greedy-IoU-Zuordnung Detektion→Track, plus ein Center-Distanz-Fallback (MAX_CENTER_DIST = 0.25). Der Fallback ist nötig und nicht optional: beim Übergang stehend→liegend ändert sich die bbox-Form so stark, dass die IoU zwischen zwei Frames unter die Schwelle fällt — ohne Fallback würde der Track genau im Sturz abreißen und die Abstiegs-Historie verlieren (Sturz würde NICHT erkannt). Das fiel beim Durchrechnen der Tests auf (IoU stehend↔liegend ≈ 0.10—0.14 < 0.2).
Cooldown ist pro Person (nicht pro Kamera): so kann jede Person einmal pro Cooldown auslösen; ein zweiter, gleichzeitiger Sturz einer anderen Person geht nicht verloren.
Konkrete Default-Parameter — UNSICHER, per OBS zu kalibrieren
Diese Werte sind begründete Startwerte, keine validierten Konstanten (kameraabhängig: Blickwinkel, Abstand, Auflösung, Bildrate). Alle in [detection] konfigurierbar:
| Parameter | Default | Unsicherheit / Annahme |
|---|---|---|
|
0.35 /s |
Hoch unsicher. Annahme: ein Sturz überquert ~0.3—0.6 der Bildhöhe in <1 s. Reiterstürze (vom Pferd, teilverdeckt) habe ich NICHT messen können. |
|
0.5 |
Kurz genug für den Peak-Abstieg, lang genug gegen Jitter — nicht empirisch belegt. |
|
1.5 |
Spanne Abstieg→bestätigtes Liegen. Geschätzt. |
|
0.4 |
Debounce gegen Flacker/kurzes Bücken. Geschätzt. |
|
0.2 |
Üblicher SORT-Bereich (0.2—0.3); bewusst niedrig wegen Formänderung. |
|
2.0 |
Wie lange ein verdeckter Track überlebt. Geschätzt. |
|
0.25 |
Pro-Frame-Zentrumssprung bei ~10 FPS; bei sehr schnellen/nahen Stürzen evtl. zu klein. |
Weitere offene Unsicherheiten:
-
Maß = bbox-Zentrum-y. Robust und immer verfügbar, aber der Kopf (Box-Oberkante) fällt bei einem Sturz stärker als das Zentrum → ggf. weniger empfindlich. Alternative (Kopf-/ Nasen-Keypoint) wäre empfindlicher, aber rauschanfälliger/verdeckungsanfällig. Bewusst konservativ gewählt; bei zu vielen verpassten Stürzen im OBS-Test umstellen.
-
FPS-Abhängigkeit: Geschwindigkeit/Zeiten sind Wall-Clock-basiert (FPS-robust). Aber bei sehr niedriger realer FPS (<~3) kann das
velocity_windowzu wenige Samples enthalten. -
Reiter-Spezifik nicht modelliert: Pferd-Verdeckung, Reiter-auf-Pferd vs. gestürzt — das Pose-Modell ist generisch (Person). Kann zu Fehlklassifikation der Pose führen.
Warum kein VLM (mistral.rs) — bestätigt
Wie zuvor entschieden: Echtzeit-Detektor bleibt YOLO-Pose + Geometrie/Dynamik. mistral.rs wäre auf dem Pi 5 zu langsam und nicht-deterministisch. Diese Dynamik-Lösung ist klein, deterministisch und vollständig unit-getestet (9 Tests).
Begründung der KISS/DRY-Entscheidungen:
-
iouundclassify_fallauspipeline.rswiederverwendet (DRY) statt neu implementiert. -
TemporalTrackerersatzlos entfernt (kein toter Code) — die Persistenz steckt jetzt inmin_ground(zeitbasiert, FPS-robust), die Glättung in der Geschwindigkeits-Mittelung. -
MAX_CENTER_DISTals Konstante statt Config, um die OBS-relevanten Knöpfe übersichtlich zu halten (nur Dynamik-Parameter sind dort). -
now: Instantinjiziert → der gesamte Tracker ist deterministisch testbar (keinsleep).
Widerspruch / Anpassung:
-
Zu viele Fehlalarme im OBS-Test →
fall_velocity_thresholderhöhen und/odermin_ground_secondsverlängern. -
Zu viele verpasste Stürze →
fall_velocity_thresholdsenken; als nächsten Schritt das Vertikalmaß von bbox-Zentrum auf bbox-Oberkante (Kopf) umstellen (center_yindynamics.rs::update). -
Personen "verschmelzen"/Tracks springen →
track_iou_thresholdsenken oderMAX_CENTER_DISTanpassen. -
Wer das alte Verhalten (reiner Liege-Zustand ohne Dynamik) zurück will:
dynamics.rsdurch eine Variante ersetzen, die Bedingung 1 (Abstieg) weglässt — dann ist es wieder ein reiner Zustands-Detektor. -
Gewünschter Daten-getriebener Ersatz: kleines temporales Keypoint-Modell (GRU/TCN über die 17 COCO-Keypoints), trainiert mit den aufgezeichneten Clips — ersetzt die Heuristik 1—3, behält Tracking + Cooldown.
2026-06-13 — Web-Interface: optionale HTTP-Basic-Auth (opt-in, Default aus)
Kontext: Das Dashboard lief auf 0.0.0.0:8080 ohne Authentifizierung und liefert über /api/config die RTSP-Zugangsdaten im Klartext aus bzw. erlaubt das Überschreiben der config.toml. In einem offenen Turnier-WLAN ist das angreifbar.
Entscheidung: Basic-Auth über eine axum-Middleware (src/web/auth.rs), aktiv nur wenn web.username UND web.password gesetzt sind. Default (leer) = kein Schutz. Neue Dependency base64 = "0.22" (MIT OR Apache-2.0, ~1,24 Mrd. Downloads, ausgereift).
Begründung:
-
Opt-in, weil der HDMI-Kiosk (
chromium --kiosk http://localhost:8080, siehestart_kiosk.sh) bei erzwungener Auth einen Login-Dialog zeigen würde → Default aus hält Kiosk und bisheriges Verhalten unverändert (KISS, nicht-brechend). -
Eigene Mini-Middleware statt
tower-http-Auth-Feature: nur ~30 Zeilen, eine Dependency weniger, volle Kontrolle (DRY: eine reinecheck_basic_auth-Funktion, die isoliert getestet wird). -
Kein TLS, kein constant-time-Vergleich: Ohne HTTPS ist Timing-Härtung Theater; der reale Schutz für untrusted Netze ist ein TLS-Reverse-Proxy. Bewusst weggelassen (KISS) und in der
config.toml.exampledokumentiert.
Widerspruch / Anpassung:
-
Auth global erzwingen:
web.username/web.passwordin der Auslieferungs-Config setzen — dann aber Kiosk-URL aufhttp://user:pass@localhost:8080umstellen. -
Nur API/Config schützen, Dashboard/MJPEG offen lassen: die
.layer(...)ausweb/mod.rsentfernen und stattdessenroute_layerselektiv auf die/api/*- und/config-Routen anwenden. -
Echte Sicherheit für offene Netze gewünscht: TLS-Reverse-Proxy (Caddy/nginx) vorschalten statt App-seitiger Auth; dann diese Middleware ggf. ganz entfernen.
-
Dependency unerwünscht:
base64durch eine ~20-Zeilen-Eigenimplementierung ersetzen (lohnt sich i.d.R. nicht — DRY).
2026-06-13 — Cooldown zeitbasiert statt Frame-gezählt
Entscheidung: TemporalTracker speichert eine Duration + Instant des letzten Alarms; update/is_in_cooldown bekommen now: Instant injiziert.
Begründung: cooldown_frames = cooldown_seconds * 10 nahm fest 10 FPS an. Der Pi 5 erreicht real weniger → Cooldown wurde unkontrolliert zu lang. Wall-Clock entkoppelt die Sperre von der FPS-Rate. now als Parameter hält den Tracker deterministisch testbar (kein echtes sleep in Tests).
Widerspruch / Anpassung: Soll der Cooldown bewusst frame-basiert sein (z.B. exakt N Auswertungen), die alte Zähl-Logik wiederherstellen. Das gleitende Fenster (window/min_hits) ist absichtlich frame-basiert geblieben — das ist ein Glättungs- filter über aufeinanderfolgende Auswertungen, kein Wall-Clock-Konzept.
2026-06-13 — Modellpfad konfigurierbar + Fail-fast
Entscheidung: detection.model_path in der Config (serde(default) = models/yolov8n-pose.onnx); load_session prüft Existenz vorab.
Begründung: Hardcodierter Pfad; fehlendes Modell führte zu stillem Dauer-Sleep, das System „lief" ohne Erkennung. Jetzt klare, handlungsweisende Meldung + sichtbares „INAKTIV"-Log. serde(default) hält bestehende Configs ohne das Feld lauffähig.
Widerspruch / Anpassung: Soll ein fehlendes Modell den Prozess hart beenden (statt weiterlaufen mit aktivem Web-UI), den Sleep-Loop in detection/mod.rs::start durch std::process::exit(1) ersetzen.
2026-06-13 — Alert: Beep von TTS entkoppeln
Entscheidung: prepare_beep() (Pflicht, reines Rust) getrennt von prepare_voice() (best-effort, Piper+ffmpeg). Fehlt TTS, wird nur der Beep gefunkt.
Begründung: Bisher deaktivierte fehlendes Piper die GESAMTE Alarmierung — ein erkannter Sturz wäre nicht gefunkt worden (sicherheitskritisch). Der Beep allein ist ein gültiger Notalarm.
Widerspruch / Anpassung: Soll ohne funktionierende Sprachansage GAR nicht alarmiert werden (z.B. weil nur die gesprochene Warnung akzeptiert wird), in AlertManager::run auch prepare_voice-Fehler zum return führen lassen.
2026-06-13 — Sturzsignal B: Torso-Winkel statt absoluter Y-Vergleiche
Entscheidung: Signal B bewertet die Neigung der Schulter-Hüfte-Achse gegen die Bild-Vertikale (atan2, Schwelle TORSO_FALL_ANGLE_DEG = 45°); Nase nicht mehr nötig.
Begründung: Die alte Logik (shoulder_y > hip_y*0.8) verglich absolute Bild- koordinaten → positionsabhängig, und verlangte eine sichtbare Nase → verpasste Stürze mit verdecktem Gesicht (gefährliche False-Negatives). Der Winkel ist positions- und letterbox-invariant.
Widerspruch / Anpassung: Schwelle zu empfindlich/zu träge → TORSO_FALL_ANGLE_DEG anpassen (kleiner = empfindlicher). Bei Bedarf in die DetectionConfig heben (analog fall_ratio_threshold), falls Feldjustierung ohne Neu-Build gewünscht ist.
Offen / bewusst aufgeschoben (brauchen menschliche Design-Entscheidung)
-
Sturz-Dynamik + Per-Person-Tracking: ERLEDIGT (siehe Eintrag oben,
detection/dynamics.rs). Offen bleibt die empirische Kalibrierung der Parameter per OBS-Test und optional ein daten-getriebenes temporales Keypoint-Modell (GRU/TCN) als Ersatz der Heuristik. -
mistral.rs wurde als Echtzeit-Detektor-Ersatz verworfen (Latenz auf Pi 5, generativer Text statt Geometrie, Halluzinationsrisiko); bestenfalls asynchroner Off-Device-Verifikations-Layer pro Event.
-
Discovery probiert nur 1 Credential/Pfad und liefert eine geratene URL.
-
Doppelter JPEG-Roundtrip Capture→Detection (perf; braucht Messung vor Umbau).