Phase 0: Repo-Gerüst, Dokumentation und Machbarkeits-Spike

Vier Beweise, die das Projekt tragen, als eigenständig lauffähige Skripte:

  S1  Plasma Mobile in proot über genestetes kwin_wayland im X11-Backend.
      Fällt selbsttätig auf startplasmamobile und dann auf XFCE zurück, damit
      ein Fehlschlag eindeutig Plasma zuzuordnen ist und nicht der Umgebung.
  S2  Mikrofon über PulseAudio mit OpenSL-Quelle. Misst den Pegel statt nur die
      Dateigröße — der typische Fehlschlag ist eine formal korrekte WAV-Datei
      voller Stille.
  S3  Virtuelle PipeWire-Kamera per GStreamer-pipewiresink, ohne Kernel-Modul.
      Setzt die Firefox-Einstellung vorab, damit der Test nicht an einem
      vergessenen Häkchen scheitert.
  S4  Intent-Handoff mit lauffähiger Mini-Bridge: xdg-open-Handler für geo:,
      tel: und sms:, Token-Authentifizierung, feste Liste erlaubter Schemata.

provision/provision.sh setzt die Hintergrundbeschränkungen per ADB und benennt,
was ADB nicht kann und von Hand erledigt werden muss.

Dokumentiert sind auch die sieben verworfenen Wege — Waydroid, AVF, QEMU, SIP,
APK-Daten-Redirect, Fenster-Durchreichung und der Android-Server — mit Belegen,
damit sie nicht erneut durchdacht werden müssen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan
2026-08-09 17:25:36 +02:00
co-authored by Claude Opus 5
commit ad2c98443b
21 changed files with 2818 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
# Phase 0 — Grundinstallation in Termux.
#
# Richtet Termux-X11, den GPU-Pfad und ein Debian-13-Rootfs ein, damit die Beweise
# S1 bis S4 danach laufen können. Idempotent: mehrfaches Ausführen ist unschädlich.
#
# Ausführen IN TERMUX auf dem Telefon: bash spike/00-bootstrap-termux.sh
set -uo pipefail
source "$(dirname "$0")/lib.sh"
require_termux
DISTRO=debian
BIND_ROOT="$REPO_ROOT"
log "HPOS Phase 0 — Bootstrap"
log "Protokoll: $LOG"
geraete_steckbrief
# ── Repositories ─────────────────────────────────────────────────────────────
# x11-repo bringt Termux-X11 und virglrenderer, tur-repo die Mesa-/Turnip-Pakete.
section "Paketquellen"
try pkg update -y
try pkg install -y x11-repo tur-repo
try pkg update -y
# ── Basispakete ──────────────────────────────────────────────────────────────
# Einzeln installiert und einzeln protokolliert: Paketnamen wandern in Termux
# gelegentlich, und wir wollen wissen, WELCHES Paket fehlt, statt nur dass etwas fehlt.
section "Basispakete"
BASIS=(termux-x11-nightly pulseaudio proot-distro termux-api git python openssl termux-tools)
for p in "${BASIS[@]}"; do
if try pkg install -y "$p"; then ok "$p"; else fail "$p — fehlt, betroffene Beweise siehe unten"; fi
done
# ── GPU-Pfad ─────────────────────────────────────────────────────────────────
# Adreno 6xx/7xx → Turnip greift direkt auf KGSL zu, das ist der schnellste Weg.
# Mali und alles andere → virgl/ANGLE. Wir installieren beides und entscheiden
# zur Laufzeit; das Ergebnis landet im GPU-Profil.
section "GPU-Pakete"
GPU_PKGS=(virglrenderer-android vulkan-loader-android vulkan-tools angle-android
mesa-vulkan-icd-wrapper mesa-zink virglrenderer-mesa-zink)
for p in "${GPU_PKGS[@]}"; do
if try pkg install -y "$p"; then ok "$p"; else warn "$p nicht verfügbar — kein Beinbruch"; fi
done
section "GPU erkennen"
PLATFORM="$(getprop ro.board.platform 2>/dev/null)"
HARDWARE="$(getprop ro.hardware 2>/dev/null)"
GPU_PROFIL="virgl" # konservative Vorgabe
if have vulkaninfo; then
vulkaninfo --summary >>"$LOG" 2>&1 || true
GPU_NAME="$(vulkaninfo --summary 2>/dev/null | grep -m1 -i 'deviceName' | cut -d= -f2- | xargs)"
else
GPU_NAME=""
fi
log " Plattform = ${PLATFORM:-?}"
log " Hardware = ${HARDWARE:-?}"
log " GPU = ${GPU_NAME:-unbekannt}"
case "${GPU_NAME}${PLATFORM}" in
*[Aa]dreno*|*qcom*|*sm[0-9]*|*msm*)
GPU_PROFIL="turnip"
ok "Adreno erkannt → Turnip-Profil (direkter KGSL-Zugriff, schnellster Weg)"
;;
*[Mm]ali*)
GPU_PROFIL="virgl"
warn "Mali erkannt → virgl/ANGLE-Profil. Spürbar langsamer als Turnip."
;;
*)
warn "GPU nicht eindeutig erkannt → virgl als Vorgabe."
hint "Bitte im Spike-Protokoll vermerken, welcher Pfad tatsächlich getragen hat."
;;
esac
# Das Profil ist eine Datei, kein Sonderfall im Code — so bleibt es später
# erweiterbar, ohne dass jemand Skripte anfassen muss.
mkdir -p "$REPO_ROOT/rootfs/profiles"
PROFIL_DATEI="$OUT_DIR/gpu-profil.env"
{
echo "# Automatisch erkannt von 00-bootstrap-termux.sh"
echo "HPOS_GPU_PROFIL=$GPU_PROFIL"
echo "HPOS_GPU_NAME=\"${GPU_NAME:-unbekannt}\""
echo "HPOS_PLATFORM=\"${PLATFORM:-unbekannt}\""
} >"$PROFIL_DATEI"
ok "GPU-Profil geschrieben: $PROFIL_DATEI ($GPU_PROFIL)"
# ── Termux-Eigenschaften ─────────────────────────────────────────────────────
# allow-external-apps ist die Voraussetzung dafür, dass die Bridge-APK später
# Kommandos in Termux anstoßen darf. Hier gesetzt, damit Phase 2 es vorfindet.
section "Termux-Eigenschaften"
TP="$HOME/.termux/termux.properties"
mkdir -p "$(dirname "$TP")"
touch "$TP"
if grep -q '^allow-external-apps' "$TP" 2>/dev/null; then
ok "allow-external-apps bereits gesetzt"
else
echo 'allow-external-apps = true' >>"$TP"
ok "allow-external-apps = true gesetzt"
hint "Wird erst nach 'termux-reload-settings' oder Neustart von Termux wirksam."
fi
try termux-reload-settings
section "Speicherzugriff"
if [ -d "$HOME/storage" ]; then
ok "Speicherzugriff bereits eingerichtet"
else
warn "termux-setup-storage wird gleich einen Berechtigungsdialog zeigen — bitte erlauben."
try termux-setup-storage
sleep 2
[ -d "$HOME/storage" ] && ok "Speicherzugriff eingerichtet" || fail "Speicherzugriff fehlt — Backup nach /sdcard geht später nicht"
fi
# ── Wachhalten ───────────────────────────────────────────────────────────────
# Ohne Wake-Lock schläft der Stack weg, sobald der Bildschirm aus geht.
# Die herstellerseitigen Maßnahmen kommen zusätzlich, siehe provision/.
section "Wake-Lock"
try termux-wake-lock && ok "Wake-Lock aktiv" || warn "Wake-Lock nicht gesetzt"
# ── Debian-Rootfs ────────────────────────────────────────────────────────────
section "Debian-Rootfs"
if proot-distro list --installed 2>/dev/null | grep -q "^$DISTRO\b" \
|| [ -d "$PREFIX/var/lib/proot-distro/installed-rootfs/$DISTRO" ]; then
ok "$DISTRO bereits installiert"
else
log " Installiere $DISTRO — das dauert einige Minuten."
need proot-distro install "$DISTRO"
ok "$DISTRO installiert"
fi
section "Debian-Grundausstattung"
# Der Bind macht das Repo im Rootfs unter /hpos sichtbar, damit die inneren
# Skripte dieselben Dateien benutzen und Ergebnisse am selben Ort landen.
proot-distro login "$DISTRO" --bind "$BIND_ROOT:/hpos" -- bash -lc '
set -uo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y --no-install-recommends \
ca-certificates locales dbus-x11 sudo nano less procps \
mesa-utils vulkan-tools pulseaudio-utils
# Deutsche und englische Locale, sonst meckert die halbe Desktop-Software.
sed -i "s/^# *\(de_DE.UTF-8\|en_US.UTF-8\)/\1/" /etc/locale.gen
locale-gen >/dev/null 2>&1
echo "--- Debian-Version ---"; cat /etc/debian_version
' 2>&1 | tee -a "$LOG"
# ── Abschluss ────────────────────────────────────────────────────────────────
section "Bereit"
log "GPU-Profil : $GPU_PROFIL"
log "Rootfs : $DISTRO"
log "Repo im Rootfs sichtbar unter /hpos"
log ""
log "Nächste Schritte, in dieser Reihenfolge:"
log " bash spike/01-s1-plasma-mobile.sh # der wackligste Beweis, deshalb zuerst"
log " bash spike/02-s2-mikrofon.sh"
log " bash spike/03-s3-pipewire-kamera.sh"
log " bash spike/04-s4-intent-handoff.sh"
log ""
log "Vor dem Übernacht-Test bitte provision/provision.sh vom PC laufen lassen"
log "und die herstellerseitigen Handgriffe aus docs/machbarkeit.md erledigen."
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Phase 0 · Beweis S1 — Läuft Plasma Mobile in proot?
#
# Der wackligste Beweis des ganzen Projekts, deshalb zuerst. Plasma Mobile will
# Wayland, und KWin will normalerweise DRM/KMS und logind — beides gibt es ohne Root
# nicht. Die Wette: KWins X11-Backend braucht weder DRM noch Seat, wenn es genestet
# in Termux-X11 läuft.
#
# Scheitert der Beweis, wird XFCE der Hauptweg. Das ist kein Weltuntergang, ändert aber
# Kamera-Portal, Bildschirmtastatur und Telefonie-Oberfläche — siehe docs/spike-protokoll.md.
#
# Ausführen IN TERMUX: bash spike/01-s1-plasma-mobile.sh
set -uo pipefail
source "$(dirname "$0")/lib.sh"
require_termux
DISTRO=debian
# Volle Panel-Auflösung wäre 3120x1440. Für einen Telefon-Desktop ist das zu viel
# Fläche — allein das Zeichnen frisst die Leistung. Überschreibbar per Umgebung.
BREITE="${HPOS_BREITE:-1080}"
HOEHE="${HPOS_HOEHE:-2340}"
[ -f "$OUT_DIR/gpu-profil.env" ] && source "$OUT_DIR/gpu-profil.env"
GPU_PROFIL="${HPOS_GPU_PROFIL:-virgl}"
log "HPOS Phase 0 · S1 — Plasma Mobile in proot"
log "Auflösung: ${BREITE}x${HOEHE} GPU-Profil: $GPU_PROFIL"
log "Protokoll: $LOG"
# ── Alte Reste einsammeln ────────────────────────────────────────────────────
section "Aufräumen"
pkill -f termux-x11 2>/dev/null && ok "alten X-Server beendet" || hint "kein alter X-Server"
pkill -f virgl_test_server 2>/dev/null && ok "alten virgl-Server beendet" || true
sleep 1
# ── X-Server ─────────────────────────────────────────────────────────────────
section "Termux-X11 starten"
if ! have termux-x11; then
fail "termux-x11 fehlt. Bitte 00-bootstrap-termux.sh laufen lassen."
exit 1
fi
termux-x11 :0 >>"$LOG" 2>&1 &
X11_PID=$!
sleep 3
if kill -0 "$X11_PID" 2>/dev/null; then
ok "X-Server läuft (PID $X11_PID)"
else
fail "X-Server sofort beendet — siehe Protokoll"
exit 1
fi
# Die App muss im Vordergrund sein, sonst sieht man nichts und Android drosselt.
log " Hole die Termux:X11-App in den Vordergrund …"
try am start --user 0 -n com.termux.x11/com.termux.x11.MainActivity
sleep 2
# ── GPU-Server ───────────────────────────────────────────────────────────────
section "GPU-Pfad vorbereiten ($GPU_PROFIL)"
if [ "$GPU_PROFIL" = "virgl" ] && have virgl_test_server_android; then
virgl_test_server_android >>"$LOG" 2>&1 &
sleep 1
ok "virgl-Server gestartet"
elif [ "$GPU_PROFIL" = "turnip" ]; then
ok "Turnip greift direkt auf KGSL zu — kein Proxy nötig"
else
warn "Kein GPU-Server aktiv, Software-Rendering wahrscheinlich"
fi
# ── Audio, damit der Desktop nicht schon daran scheitert ─────────────────────
section "PulseAudio"
pulseaudio --kill 2>/dev/null || true
try pulseaudio --start --exit-idle-time=-1 \
--load="module-native-protocol-tcp auth-ip-acl=127.0.0.1 auth-anonymous=1" \
&& ok "PulseAudio läuft" || warn "PulseAudio nicht gestartet — für S1 nicht kritisch"
# ── Übergabe an Debian ───────────────────────────────────────────────────────
section "Plasma Mobile starten"
log " Ab hier läuft alles im Debian-Rootfs. Erster Lauf lädt gut 1 GB Pakete."
log " Auf dem Telefon in die Termux:X11-App wechseln, um das Ergebnis zu sehen."
log ""
HPOS_GPU_PROFIL="$GPU_PROFIL" HPOS_BREITE="$BREITE" HPOS_HOEHE="$HOEHE" \
proot-distro login "$DISTRO" --shared-tmp --bind "$REPO_ROOT:/hpos" -- \
bash /hpos/spike/inner/s1-plasma.sh 2>&1 | tee -a "$LOG"
verdict "S1" "Zeigt die Termux:X11-App eine bedienbare Plasma-Mobile-Oberfläche?
Prüfe: reagiert Touch, erscheint die Maliit-Tastatur beim Tippen in ein Feld?"
log "Aufräumen: pkill -f termux-x11 ; pkill -f virgl_test_server"
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# Phase 0 · Beweis S2 — Erreicht das Mikrofon die Linux-Seite?
#
# Der stillste K.-o.-Punkt des Projekts. Ohne Mikrofon im Linux gibt es keine
# Videocalls in Element oder im Browser, keine Sprachaufnahme, keine Spracheingabe.
# Die Wette: Termux' PulseAudio kann über OpenSL ES eine Aufnahmequelle anbieten,
# die per TCP in den proot durchgereicht wird.
#
# Ausführen IN TERMUX: bash spike/02-s2-mikrofon.sh
set -uo pipefail
source "$(dirname "$0")/lib.sh"
require_termux
DISTRO=debian
DAUER="${HPOS_DAUER:-6}"
WAV="$OUT_DIR/s2-aufnahme.wav"
log "HPOS Phase 0 · S2 — Mikrofon im Linux"
log "Protokoll: $LOG"
# ── Berechtigung ─────────────────────────────────────────────────────────────
# Ohne erteilte Mikrofonberechtigung liefert die OpenSL-Quelle Stille statt eines
# Fehlers — der verwirrendste denkbare Fehlschlag. Deshalb vorher erzwingen.
section "Mikrofonberechtigung"
if have termux-microphone-record; then
log " Kurzer Test über Termux:API, um den Berechtigungsdialog auszulösen …"
try termux-microphone-record -d -l 2 -f "$OUT_DIR/s2-vortest.m4a"
sleep 3
try termux-microphone-record -q
if [ -s "$OUT_DIR/s2-vortest.m4a" ]; then
ok "Termux:API kann aufnehmen — Berechtigung liegt vor"
else
fail "Termux:API konnte nicht aufnehmen"
hint "Einstellungen → Apps → Termux:API → Berechtigungen → Mikrofon erlauben."
hint "Ohne das ist S2 nicht aussagekräftig."
fi
else
warn "termux-microphone-record fehlt (Paket termux-api). Weiter ohne Vortest."
fi
# ── PulseAudio mit Aufnahmequelle ────────────────────────────────────────────
section "PulseAudio mit OpenSL-Quelle"
pulseaudio --kill 2>/dev/null || true
sleep 1
# module-sles-source ist der Teil, der wackelt: er existiert nicht in jedem
# Termux-Build. Deshalb erst mit, dann ohne — und der Unterschied wird protokolliert.
MIT_QUELLE=0
if pulseaudio --start --exit-idle-time=-1 \
--load="module-sles-source" \
--load="module-native-protocol-tcp auth-ip-acl=127.0.0.1 auth-anonymous=1" \
>>"$LOG" 2>&1; then
MIT_QUELLE=1
ok "PulseAudio mit module-sles-source gestartet"
else
warn "module-sles-source ließ sich nicht laden — starte ohne"
try pulseaudio --start --exit-idle-time=-1 \
--load="module-native-protocol-tcp auth-ip-acl=127.0.0.1 auth-anonymous=1"
fi
sleep 2
section "Verfügbare Quellen"
if have pactl; then
pactl list sources short 2>&1 | tee -a "$LOG" | sed 's/^/ /'
QUELLEN=$(pactl list sources short 2>/dev/null | wc -l)
if [ "$QUELLEN" -gt 0 ]; then
ok "$QUELLEN Aufnahmequelle(n) gemeldet"
else
fail "Keine einzige Aufnahmequelle — S2 wird so nicht gelingen"
hint "Falls module-sles-source fehlt, ist dieser Termux-Build ohne OpenSL-Aufnahme gebaut."
fi
else
warn "pactl fehlt, kann Quellen nicht auflisten"
fi
# ── Aufnahme im Debian ───────────────────────────────────────────────────────
section "Aufnahme aus dem Debian heraus"
log " ${DAUER} Sekunden werden aufgenommen — bitte jetzt laut und deutlich sprechen."
log ""
sleep 1
proot-distro login "$DISTRO" --shared-tmp --bind "$REPO_ROOT:/hpos" -- bash -lc "
set -uo pipefail
export PULSE_SERVER=127.0.0.1
echo ' Quellen aus Sicht des Debian:'
pactl list sources short 2>&1 | sed 's/^/ /' || echo ' (pactl lieferte nichts)'
echo ' Nehme auf …'
timeout $((DAUER + 4)) parecord --channels=1 --rate=16000 --file-format=wav \
/hpos/spike/out/$(basename "$WAV") &
REC=\$!
sleep $DAUER
kill \$REC 2>/dev/null
wait \$REC 2>/dev/null
echo ' Aufnahme beendet.'
" 2>&1 | tee -a "$LOG"
# ── Auswertung ───────────────────────────────────────────────────────────────
# Eine Datei zu erzeugen beweist nichts: die häufigste Fehlerform ist eine
# formal korrekte WAV-Datei voller Stille. Deshalb Größe UND Inhalt prüfen.
section "Auswertung"
if [ ! -s "$WAV" ]; then
fail "Keine Aufnahmedatei entstanden → S2 gescheitert"
else
GROESSE=$(stat -c%s "$WAV" 2>/dev/null || echo 0)
ok "Datei vorhanden: $WAV ($GROESSE Bytes)"
ERWARTET=$((DAUER * 16000 * 2 * 8 / 10))
if [ "$GROESSE" -lt "$ERWARTET" ]; then
warn "Deutlich kleiner als erwartet (~$ERWARTET Bytes) — vermutlich abgebrochen"
fi
# Stilletest. Der typische Fehlschlag ist eine formal korrekte WAV-Datei voller
# Nullen — sie sieht in jeder Dateiliste richtig aus und enthält doch nichts.
# Deshalb wird der Pegel gemessen, nicht nur die Größe.
if have python; then
python - "$WAV" <<'PY' 2>&1 | tee -a "$LOG"
import array, math, sys, wave
# Bewusst ohne audioop: das Modul ist in Python 3.13 entfernt worden, und Termux
# liegt regelmäßig auf der aktuellen Version.
try:
with wave.open(sys.argv[1]) as w:
breite = w.getsampwidth()
roh = w.readframes(w.getnframes())
if breite == 1:
werte = array.array("b", bytes(b - 128 for b in roh))
elif breite == 2:
werte = array.array("h", roh[: len(roh) // 2 * 2])
else:
raise ValueError(f"{breite} Byte je Abtastwert werden hier nicht ausgewertet")
if not werte:
raise ValueError("keine Abtastwerte enthalten")
pegel = int(math.sqrt(sum(v * v for v in werte) / len(werte)))
spitze = max(abs(v) for v in werte)
print(f" Effektivpegel: {pegel} Spitze: {spitze}")
if spitze < 40:
print(" ✗ Praktisch Stille — die Quelle liefert keine echten Daten.")
print(" Das ist der typische Fehlschlag: Datei ja, Ton nein.")
print(" Prüfen: Mikrofonberechtigung für Termux:API, und ob")
print(" module-sles-source in diesem Termux-Build überhaupt existiert.")
else:
print(" ✓ Echtes Signal aufgenommen.")
except Exception as e:
print(f" ! Datei nicht auswertbar: {e}")
PY
fi
log ""
log " Zum Gegenhören: pactl upload-sample nicht nötig — einfach abspielen:"
log " proot-distro login $DISTRO -- env PULSE_SERVER=127.0.0.1 paplay /hpos/spike/out/$(basename "$WAV")"
fi
log ""
log " module-sles-source geladen: $([ "$MIT_QUELLE" = 1 ] && echo ja || echo NEIN)"
verdict "S2" "Ist auf der Aufnahme deine Stimme hörbar?
Abspielen mit dem Befehl oben. Eine Datei ohne Ton zählt als gescheitert."
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Phase 0 · Beweis S3 — Virtuelle Kamera ohne Kernel-Modul.
#
# v4l2loopback wäre der klassische Weg, ist aber ein Kernel-Modul und damit ohne Root
# ausgeschlossen. Die Wette: ein GStreamer-pipewiresink mit media.class=Video/Source
# erzeugt einen Kamera-Node komplett im Userspace, den Firefox über das Portal annimmt.
#
# Belegt der Beweis, funktionieren Videocalls im Browser und in Element.
# Siehe: https://www.ideasonboard.com/news/pipewire-is-the-new-v4l2loopback/
#
# Ausführen IN TERMUX: bash spike/03-s3-pipewire-kamera.sh
set -uo pipefail
source "$(dirname "$0")/lib.sh"
require_termux
DISTRO=debian
log "HPOS Phase 0 · S3 — Virtuelle PipeWire-Kamera"
log "Protokoll: $LOG"
log ""
log "Voraussetzung: ein laufender X-Server aus S1."
log "Falls nicht, in einer zweiten Sitzung: termux-x11 :0 &"
log ""
section "Übergabe an Debian"
proot-distro login "$DISTRO" --shared-tmp --bind "$REPO_ROOT:/hpos" -- \
bash /hpos/spike/inner/s3-kamera.sh 2>&1 | tee -a "$LOG"
verdict "S3" "Zeigt Firefox auf webcamtests.com ein Bild aus der Quelle 'HPOS-Testkamera'?
Ein reines Auftauchen im Auswahlmenü genügt nicht — es muss Bild kommen."
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Phase 0 · Beweis S4 — Intent-Handoff vom Linux zur Android-App.
#
# Die Wette: statt Android-Apps ins Linux zu streamen, übergibt das Linux eine
# Absicht und die App öffnet nativ im Vollbild. Für Navigation ist das nicht der
# Kompromiss, sondern die bessere Lösung — volles GPU, volles GPS, und Overlay-Apps
# funktionieren, was über einem gestreamten Fenster gar nicht ginge.
#
# Gebaut wird das nicht pro App, sondern einmal als xdg-open-Handler. Damit löst
# jede Linux-Anwendung — Adressbuch, Browser, Kalender — die richtige Android-App aus,
# ohne selbst etwas von der Bridge zu wissen.
#
# Ausführen IN TERMUX: bash spike/04-s4-intent-handoff.sh
set -uo pipefail
source "$(dirname "$0")/lib.sh"
require_termux
DISTRO=debian
ZIEL="${HPOS_ZIEL:-Brandenburger Tor, Berlin}"
# Paketname einer Begleit-App mit Overlay, die vor der Navigation starten soll.
# Beispiel: HPOS_BEGLEIT_APP=de.blitzer bash spike/04-s4-intent-handoff.sh
BEGLEIT_APP="${HPOS_BEGLEIT_APP:-}"
log "HPOS Phase 0 · S4 — Intent-Handoff"
log "Navigationsziel: $ZIEL"
log "Begleit-App: ${BEGLEIT_APP:-(keine)}"
log "Protokoll: $LOG"
# ── Vorbedingungen ───────────────────────────────────────────────────────────
section "Vorbedingungen"
if have am; then
ok "am vorhanden"
else
fail "am fehlt (Paket termux-tools) — ohne das geht nichts"
exit 1
fi
have python && ok "python vorhanden" || { fail "python fehlt"; exit 1; }
if [ -n "$BEGLEIT_APP" ]; then
if pm list packages 2>/dev/null | grep -q "package:$BEGLEIT_APP"; then
ok "Begleit-App $BEGLEIT_APP ist installiert"
hint "Overlay-Berechtigung nötig: Einstellungen → Apps → $BEGLEIT_APP → über anderen Apps anzeigen"
else
warn "Begleit-App $BEGLEIT_APP nicht gefunden — Overlay-Teil des Beweises entfällt"
fi
fi
# ── Mini-Bridge starten ──────────────────────────────────────────────────────
section "Mini-Bridge"
pkill -f 'bridge-mini.py' 2>/dev/null && { hint "alte Instanz beendet"; sleep 1; } || true
HPOS_BEGLEIT_APP="$BEGLEIT_APP" python "$REPO_ROOT/spike/bridge-mini.py" \
>>"$OUT_DIR/bridge-mini.log" 2>&1 &
BRIDGE_PID=$!
sleep 2
if kill -0 "$BRIDGE_PID" 2>/dev/null; then
ok "Bridge läuft auf 127.0.0.1:8474 (PID $BRIDGE_PID)"
else
fail "Bridge startet nicht:"
tail -n 20 "$OUT_DIR/bridge-mini.log" | sed 's/^/ /' | tee -a "$LOG"
exit 1
fi
# ── Direkttest, bevor Linux ins Spiel kommt ──────────────────────────────────
# Erst beweisen, dass der Intent überhaupt zieht. Scheitert es hier, liegt es an
# Android und nicht an der Brücke ins Linux — ein wichtiger Unterschied.
section "Direkttest ohne Linux"
TOKEN="$(cat "$OUT_DIR/bridge-token")"
ANTWORT="$(printf '%s\t%s\n' "$TOKEN" "geo:0,0?q=$(python -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$ZIEL")" \
| timeout 25 python -c '
import socket,sys
s=socket.create_connection(("127.0.0.1",8474),10)
s.sendall(sys.stdin.buffer.read())
s.shutdown(socket.SHUT_WR)
print(s.recv(4096).decode().strip())
' 2>&1)"
log " Antwort: $ANTWORT"
case "$ANTWORT" in
OK*) ok "Android hat den Intent angenommen — schau aufs Telefon" ;;
*) fail "Intent nicht angenommen"
hint "Ist eine Navigations-App installiert? Ohne Google Maps o. ä. gibt es keinen Empfänger." ;;
esac
sleep 6
# ── Der eigentliche Beweis: aus dem Linux heraus ─────────────────────────────
section "Aus dem Linux heraus"
HPOS_ZIEL="$ZIEL" proot-distro login "$DISTRO" --shared-tmp --bind "$REPO_ROOT:/hpos" -- \
bash /hpos/spike/inner/s4-handoff.sh 2>&1 | tee -a "$LOG"
log ""
log " Bridge-Protokoll: $OUT_DIR/bridge-mini.log"
log " Bridge beenden: kill $BRIDGE_PID"
verdict "S4" "Hat 'xdg-open geo:…' aus dem Debian heraus die Navigation auf dem Telefon gestartet?
Und lag — falls konfiguriert — das Overlay der Begleit-App darüber?"
+87
View File
@@ -0,0 +1,87 @@
# Phase 0 — Machbarkeits-Spike
Vier Annahmen tragen das gesamte Projekt. Scheitert eine, sieht alles Weitere anders
aus — und das will man **vor** dem Bau der Infrastruktur wissen, nicht danach.
Der Code hier ist bewusst Wegwerf-Code. Was zählt, sind die Protokolle: sie werden
später zur Grundlage der Setup-Skripte in `rootfs/`.
| | Beweis | Wenn er scheitert |
|---|---|---|
| **S1** | Plasma Mobile läuft in proot (genestetes KWin) | XFCE-Profil wird Hauptweg; Kamera-Portal, Bildschirmtastatur und Telefonie-Oberfläche ändern sich mit |
| **S2** | Mikrofon-Eingang erreicht die Linux-Seite | Keine Videocalls, keine Sprachaufnahme, keine Spracheingabe |
| **S3** | Virtuelle PipeWire-Kamera ohne Kernel-Modul | Kamera nur in einer eigenständigen App, keine Browser-Videocalls |
| **S4** | Intent-Handoff Linux → Android-App | Keine Navigation aus dem Linux-Adressbuch |
Dazu der **Übernacht-Test**: überlebt der Stack eine Nacht mit gesperrtem Bildschirm?
Scheitert er, ist das ein früher Warnschuss fürs Gesamtkonzept, kein Detail.
## Voraussetzungen
- Android-Telefon, Adreno 6xx/7xx bevorzugt (Turnip-Pfad)
- **Termux von F-Droid oder GitHub** — nicht aus dem Play Store, die Fassung dort ist
veraltet und unbrauchbar
- Einmalig `provision/provision.sh` vom PC, plus die dort genannten Handgriffe von Hand
- Etwa 6 GB freier Speicher (Debian-Rootfs, Plasma Mobile, Firefox)
- Geduld beim ersten Lauf: rund 1 GB Pakete
## Ablauf
```bash
# Einmalig, vom PC mit angeschlossenem Telefon
./provision/provision.sh
# Auf dem Telefon in Termux
bash spike/00-bootstrap-termux.sh
# Beweise in dieser Reihenfolge — S1 zuerst, weil er der wackligste ist
bash spike/01-s1-plasma-mobile.sh
bash spike/02-s2-mikrofon.sh
bash spike/03-s3-pipewire-kamera.sh
bash spike/04-s4-intent-handoff.sh
```
Jedes Skript fragt am Ende nach deiner Beurteilung und schreibt sie nach
`spike/out/ergebnisse.tsv`. Rohprotokolle liegen daneben in `spike/out/*.log`.
**Übertrage die Ergebnisse anschließend nach [`docs/spike-protokoll.md`](../docs/spike-protokoll.md).**
`spike/out/` ist absichtlich nicht im Repo — die Auswertung gehört dorthin, nicht die Rohdaten.
## Stellschrauben
| Variable | Vorgabe | Wofür |
|---|---|---|
| `HPOS_BREITE` / `HPOS_HOEHE` | `1080` / `2340` | Virtuelle Auflösung. Volle Panel-Auflösung ist für einen Telefon-Desktop zu viel Fläche |
| `HPOS_DAUER` | `6` | Sekunden Aufnahme in S2 |
| `HPOS_ZIEL` | Brandenburger Tor, Berlin | Navigationsziel in S4 |
| `HPOS_BEGLEIT_APP` | leer | Paketname einer Overlay-App, die in S4 vor der Navigation startet |
Beispiel:
```bash
HPOS_BEGLEIT_APP=de.blitzer HPOS_ZIEL="Kölner Dom" bash spike/04-s4-intent-handoff.sh
```
## Wenn etwas schiefgeht
**S1 startet nicht.** Das Skript probiert von selbst zwei Wege und fällt am Ende auf
XFCE zurück. Läuft XFCE, dann tragen X-Server und GPU-Pfad — der Fehlschlag liegt dann
eindeutig bei Plasma. Läuft auch XFCE nicht, liegt es an X-Server oder GPU.
**S2 erzeugt eine Datei ohne Ton.** Der häufigste und tückischste Fehlschlag. Das Skript
misst deshalb den Pegel. Ursache ist meist eine fehlende Mikrofonberechtigung für
Termux:API oder ein Termux-Build ohne `module-sles-source`.
**S3: Firefox sieht die Kamera nicht.** Prüfen, ob `xdg-desktop-portal-kde` läuft — die
GTK- und wlr-Portale können das Kamera-Portal nicht bedienen. In `about:config` muss
`media.webrtc.camera.allow-pipewire` auf `true` stehen; das Skript setzt es vorab.
**S4: kein Empfänger für den Intent.** Ohne installierte Navigations-App gibt es nichts,
was den `geo:`-Intent annehmen könnte. Das ist kein Fehler der Brücke.
## Aufräumen
```bash
pkill -f 'termux-x11|virgl_test_server|kwin_wayland|pipewire|bridge-mini.py'
pulseaudio --kill
```
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""HPOS Mini-Bridge — Prototyp des Intent-Handoffs für Beweis S4.
Läuft in Termux, lauscht auf 127.0.0.1 und übersetzt URIs vom Linux in
Android-Intents. Das ist die kleinste ehrliche Fassung dessen, was später die
Bridge-APK macht: sie speichert nichts, sie reicht durch.
Bewusst eng gehalten:
* nur 127.0.0.1, niemals extern erreichbar
* gemeinsames Token, damit nicht jede App auf dem Telefon Intents auslösen kann
* feste Liste erlaubter Schemata statt beliebiger Intents
Start: python spike/bridge-mini.py
"""
from __future__ import annotations
import os
import shlex
import socketserver
import subprocess
import sys
import time
import urllib.parse
HOST = "127.0.0.1"
PORT = int(os.environ.get("HPOS_BRIDGE_PORT", "8474"))
BASIS = os.path.dirname(os.path.abspath(__file__))
TOKEN_DATEI = os.path.join(BASIS, "out", "bridge-token")
# Paketname einer Begleit-App, die vor der Hauptanwendung starten soll — typisch
# eine App mit Overlay, das über der Navigation liegen muss. Leer = keine.
BEGLEIT_APP = os.environ.get("HPOS_BEGLEIT_APP", "").strip()
BEGLEIT_VORLAUF = float(os.environ.get("HPOS_BEGLEIT_VORLAUF", "2.5"))
# Was aus dem Linux kommen darf. Alles andere wird abgelehnt — eine offene
# Intent-Schnittstelle wäre ein Einfallstor für jede App auf dem Gerät.
ERLAUBTE_SCHEMATA = {"geo", "tel", "sms", "smsto", "mailto", "http", "https"}
def token_lesen() -> str:
"""Token holen, beim ersten Start erzeugen."""
os.makedirs(os.path.dirname(TOKEN_DATEI), exist_ok=True)
if not os.path.exists(TOKEN_DATEI):
with open(os.open(TOKEN_DATEI, os.O_CREAT | os.O_WRONLY, 0o600), "w") as f:
f.write(os.urandom(16).hex())
with open(TOKEN_DATEI) as f:
return f.read().strip()
TOKEN = token_lesen()
def am(*args: str) -> tuple[int, str]:
"""`am` aufrufen und Ergebnis zurückgeben."""
befehl = ["am", *args]
print(f"{' '.join(shlex.quote(a) for a in befehl)}", flush=True)
try:
p = subprocess.run(befehl, capture_output=True, text=True, timeout=20)
return p.returncode, (p.stdout + p.stderr).strip()
except FileNotFoundError:
return 127, "am nicht gefunden — Paket termux-tools fehlt"
except subprocess.TimeoutExpired:
return 124, "am hat nicht geantwortet"
def app_starten(paket: str) -> None:
am("start", "--user", "0", "-a", "android.intent.action.MAIN",
"-c", "android.intent.category.LAUNCHER", "-p", paket)
def navigation(ziel: str) -> tuple[int, str]:
"""Turn-by-turn direkt starten statt nur die Karte zu zeigen.
Vorher optional die Begleit-App, damit deren Overlay schon steht, wenn die
Navigation in den Vordergrund kommt — andersherum legt sich die Navigation
darüber und das Overlay ist verdeckt.
"""
if BEGLEIT_APP:
print(f" Begleit-App vorab: {BEGLEIT_APP}", flush=True)
app_starten(BEGLEIT_APP)
time.sleep(BEGLEIT_VORLAUF)
uri = "google.navigation:q=" + urllib.parse.quote(ziel) + "&mode=d"
return am("start", "--user", "0", "-a", "android.intent.action.VIEW", "-d", uri)
def behandeln(zeile: str) -> str:
"""Eine Anfrage verarbeiten. Format: <token>\\t<uri>"""
teile = zeile.split("\t", 1)
if len(teile) != 2:
return "FEHLER ungueltiges-format"
token, uri = teile[0].strip(), teile[1].strip()
if token != TOKEN:
print(" ! Anfrage mit falschem Token abgelehnt", flush=True)
return "FEHLER token"
if not uri:
return "FEHLER leere-uri"
schema = uri.split(":", 1)[0].lower()
# geo:-URIs mit Suchbegriff bedeuten in der Praxis fast immer "dahin navigieren",
# nicht "Karte anschauen". Deshalb wird daraus Turn-by-turn.
if schema == "geo":
query = urllib.parse.parse_qs(urllib.parse.urlparse(uri).query).get("q", [""])[0]
if query:
rc, aus = navigation(query)
return f"OK navigation {rc} {aus}" if rc == 0 else f"FEHLER am {rc} {aus}"
if schema == "tel":
# DIAL statt CALL: die Nummer erscheint im Wähler, gewählt wird bewusst.
rc, aus = am("start", "--user", "0", "-a", "android.intent.action.DIAL", "-d", uri)
return f"OK dial {rc} {aus}" if rc == 0 else f"FEHLER am {rc} {aus}"
if schema in ERLAUBTE_SCHEMATA:
rc, aus = am("start", "--user", "0", "-a", "android.intent.action.VIEW", "-d", uri)
return f"OK view {rc} {aus}" if rc == 0 else f"FEHLER am {rc} {aus}"
print(f" ! Schema abgelehnt: {schema}", flush=True)
return f"FEHLER schema-nicht-erlaubt {schema}"
class Handler(socketserver.StreamRequestHandler):
timeout = 10
def handle(self) -> None:
try:
zeile = self.rfile.readline(4096).decode("utf-8", "replace")
except Exception as e:
print(f" ! Lesefehler: {e}", flush=True)
return
if not zeile:
return
antwort = behandeln(zeile)
print(f"{antwort}", flush=True)
try:
self.wfile.write((antwort + "\n").encode())
except Exception:
pass
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
def main() -> int:
print("HPOS Mini-Bridge (Prototyp für Beweis S4)")
print(f" Lauscht auf {HOST}:{PORT}")
print(f" Token-Datei {TOKEN_DATEI}")
print(f" Begleit-App {BEGLEIT_APP or '(keine)'}")
print(" Beenden mit Strg+C")
print()
try:
with Server((HOST, PORT), Handler) as srv:
srv.serve_forever()
except KeyboardInterrupt:
print("\nBeendet.")
except OSError as e:
print(f"Start fehlgeschlagen: {e}", file=sys.stderr)
print(f"Läuft schon etwas auf Port {PORT}?", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+139
View File
@@ -0,0 +1,139 @@
#!/bin/bash
# Läuft INNERHALB des Debian-Rootfs, aufgerufen von 01-s1-plasma-mobile.sh.
# Direkt aufrufen ergibt keinen Sinn — die Umgebung kommt vom äußeren Skript.
set -uo pipefail
GPU_PROFIL="${HPOS_GPU_PROFIL:-virgl}"
BREITE="${HPOS_BREITE:-1080}"
HOEHE="${HPOS_HOEHE:-2340}"
echo "── Debian-Seite: Plasma Mobile ──"
export DEBIAN_FRONTEND=noninteractive
export DISPLAY=:0
export XDG_RUNTIME_DIR=/tmp/hpos-runtime
export PULSE_SERVER=127.0.0.1
export LANG=de_DE.UTF-8
mkdir -p "$XDG_RUNTIME_DIR" && chmod 700 "$XDG_RUNTIME_DIR"
# Plasma Mobile ist Touch-first: ohne diese Variablen kommt eine Desktop-Skalierung
# heraus, die auf einem Telefon unbedienbar ist.
export QT_QPA_PLATFORM=wayland
export QT_QUICK_CONTROLS_MOBILE=1
export PLASMA_PLATFORM=phone
export KDE_FULL_SESSION=true
case "$GPU_PROFIL" in
turnip)
# Zink übersetzt OpenGL auf Vulkan, Turnip liefert Vulkan direkt aus dem
# Adreno-Treiber — zusammen der schnellste Weg ohne Root.
export MESA_LOADER_DRIVER_OVERRIDE=zink
export GALLIUM_DRIVER=zink
export TU_DEBUG=noconform
;;
virgl)
export GALLIUM_DRIVER=virpipe
export MESA_GL_VERSION_OVERRIDE=4.3
;;
esac
echo " GPU-Profil: $GPU_PROFIL"
# ── Pakete ───────────────────────────────────────────────────────────────────
# Bewusst ohne empfohlene Pakete: sonst zieht Plasma Mobile eine komplette
# Desktop-Umgebung samt Drucksystem mit, was auf einem Telefon nur Platz kostet.
if ! command -v kwin_wayland >/dev/null 2>&1; then
echo "── Installiere Plasma Mobile (einmalig, gut 1 GB) ──"
apt-get update -qq
apt-get install -y --no-install-recommends \
plasma-mobile kwin-wayland plasma-workspace maliit-keyboard \
xdg-desktop-portal-kde qtwayland5 libqt5waylandclient5 \
|| echo " ! Paketinstallation unvollständig — Details oben"
fi
for b in kwin_wayland plasmashell; do
if command -v "$b" >/dev/null 2>&1; then echo "$b vorhanden"
else echo "$b fehlt"; fi
done
# ── GPU-Nachweis, bevor der Desktop startet ──────────────────────────────────
# Wenn hier schon nichts kommt, ist ein Fehlschlag von Plasma keine Aussage über
# Plasma, sondern über den GPU-Pfad. Deshalb vorher messen.
echo "── GPU-Nachweis ──"
timeout 20 glxinfo -B 2>&1 | grep -Ei 'OpenGL (renderer|version)|Device' || echo " ! glxinfo lieferte nichts"
timeout 20 vulkaninfo --summary 2>&1 | grep -Ei 'driverName|deviceName' || echo " ! vulkaninfo lieferte nichts"
# ── D-Bus ────────────────────────────────────────────────────────────────────
if [ ! -e /run/dbus/system_bus_socket ]; then
mkdir -p /run/dbus
dbus-daemon --system --fork 2>/dev/null && echo " ✓ System-D-Bus gestartet" \
|| echo " ! System-D-Bus nicht gestartet (ohne logind oft verschmerzbar)"
fi
# ── Versuch A: KWin genestet im X11-Backend ──────────────────────────────────
# Das ist die eigentliche Wette. In diesem Modus braucht KWin weder DRM noch Seat.
echo ""
echo "── Versuch A: kwin_wayland --x11-display ──"
LOGA=/tmp/hpos-s1-versuchA.log
dbus-run-session -- kwin_wayland \
--x11-display "$DISPLAY" \
--width "$BREITE" --height "$HOEHE" \
--xwayland \
-- plasmashell -p org.kde.plasma.phoneshell \
>"$LOGA" 2>&1 &
PID_A=$!
echo " gestartet (PID $PID_A), warte 25 s auf die Oberfläche …"
sleep 25
if kill -0 "$PID_A" 2>/dev/null; then
echo " ✓ Versuch A läuft noch — jetzt in die Termux:X11-App wechseln und schauen."
echo ""
echo " Beenden mit: pkill -f kwin_wayland"
echo " Protokoll: $LOGA"
tail -n 25 "$LOGA" 2>/dev/null | sed 's/^/ /'
exit 0
fi
echo " ✗ Versuch A beendet. Letzte Zeilen:"
tail -n 40 "$LOGA" 2>/dev/null | sed 's/^/ /'
# ── Versuch B: die von Debian mitgelieferte Sitzung ──────────────────────────
# Startet KWin selbst und wählt sein Backend eigenständig. Weniger Kontrolle,
# aber manchmal weiß das Distributionsskript etwas, das wir nicht wissen.
echo ""
echo "── Versuch B: startplasmamobile ──"
if command -v startplasmamobile >/dev/null 2>&1; then
LOGB=/tmp/hpos-s1-versuchB.log
KWIN_COMPOSE=O2 dbus-run-session -- startplasmamobile >"$LOGB" 2>&1 &
PID_B=$!
sleep 25
if kill -0 "$PID_B" 2>/dev/null; then
echo " ✓ Versuch B läuft — in die Termux:X11-App wechseln."
tail -n 25 "$LOGB" 2>/dev/null | sed 's/^/ /'
exit 0
fi
echo " ✗ Versuch B beendet. Letzte Zeilen:"
tail -n 40 "$LOGB" 2>/dev/null | sed 's/^/ /'
else
echo " ! startplasmamobile nicht vorhanden"
fi
# ── Rückfallebene: XFCE, damit der Abend nicht umsonst war ───────────────────
# Beweist zumindest, dass X-Server und GPU-Pfad tragen. Damit ist ein Fehlschlag
# von S1 eindeutig Plasma zuzuordnen und nicht der Umgebung.
echo ""
echo "── Rückfallebene: XFCE zur Gegenprobe ──"
if ! command -v startxfce4 >/dev/null 2>&1; then
apt-get install -y --no-install-recommends xfce4 xfce4-terminal >/dev/null 2>&1 \
|| echo " ! XFCE-Installation fehlgeschlagen"
fi
if command -v startxfce4 >/dev/null 2>&1; then
dbus-run-session -- startxfce4 >/tmp/hpos-s1-xfce.log 2>&1 &
sleep 12
if pgrep -f xfwm4 >/dev/null 2>&1; then
echo " ✓ XFCE läuft. S1 ist gescheitert, aber X-Server und GPU-Pfad tragen."
echo " → Im Protokoll als 'S1 gescheitert, XFCE-Profil wird Hauptweg' vermerken."
else
echo " ✗ Auch XFCE startet nicht — dann liegt es an X-Server oder GPU, nicht an Plasma."
fi
fi
exit 1
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
# Läuft INNERHALB des Debian-Rootfs, aufgerufen von 03-s3-pipewire-kamera.sh.
set -uo pipefail
export DEBIAN_FRONTEND=noninteractive
export DISPLAY=:0
export XDG_RUNTIME_DIR=/tmp/hpos-runtime
export XDG_CURRENT_DESKTOP=KDE
export LANG=de_DE.UTF-8
mkdir -p "$XDG_RUNTIME_DIR" && chmod 700 "$XDG_RUNTIME_DIR"
echo "── Debian-Seite: virtuelle PipeWire-Kamera ──"
# ── Pakete ───────────────────────────────────────────────────────────────────
# xdg-desktop-portal-kde ist der Grund, warum Plasma Mobile hier im Vorteil ist:
# die GTK- und wlr-Portale können das Kamera-Portal nicht bedienen.
if ! command -v pw-cli >/dev/null 2>&1; then
echo "── Installiere PipeWire und GStreamer (einmalig) ──"
apt-get update -qq
apt-get install -y --no-install-recommends \
pipewire pipewire-pulse wireplumber pipewire-bin \
gstreamer1.0-pipewire gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-tools xdg-desktop-portal xdg-desktop-portal-kde \
|| echo " ! Paketinstallation unvollständig"
fi
# ── PipeWire starten ─────────────────────────────────────────────────────────
# Kein systemd im proot, also die Dienste von Hand — und in dieser Reihenfolge,
# sonst findet WirePlumber den Daemon nicht.
echo "── Dienste starten ──"
pkill -f 'pipewire|wireplumber|xdg-desktop-portal' 2>/dev/null; sleep 1
if [ ! -e /run/dbus/system_bus_socket ]; then
mkdir -p /run/dbus && dbus-daemon --system --fork 2>/dev/null || true
fi
eval "$(dbus-launch --sh-syntax)" 2>/dev/null || true
pipewire >/tmp/hpos-pipewire.log 2>&1 & sleep 2
pipewire-pulse >/tmp/hpos-pw-pulse.log 2>&1 & sleep 1
wireplumber >/tmp/hpos-wireplumber.log 2>&1 & sleep 2
if pgrep -x pipewire >/dev/null; then echo " ✓ pipewire läuft"; else
echo " ✗ pipewire startet nicht:"; tail -n 20 /tmp/hpos-pipewire.log | sed 's/^/ /'; exit 1
fi
pgrep -x wireplumber >/dev/null && echo " ✓ wireplumber läuft" \
|| echo " ! wireplumber läuft nicht — Nodes werden evtl. nicht verwaltet"
/usr/libexec/xdg-desktop-portal-kde >/tmp/hpos-portal-kde.log 2>&1 & sleep 1
/usr/libexec/xdg-desktop-portal >/tmp/hpos-portal.log 2>&1 & sleep 2
pgrep -f xdg-desktop-portal >/dev/null && echo " ✓ Portal läuft" \
|| echo " ! Portal läuft nicht — Firefox wird die Kamera nicht sehen"
# ── Die virtuelle Kamera ─────────────────────────────────────────────────────
# Im Spike steht ein Testbild statt der echten Kamera. Das ist Absicht: bewiesen
# werden soll der PIPEWIRE-Weg, nicht die Bridge. Später ersetzt der Bridge-Stream
# videotestsrc — sonst nichts.
echo ""
echo "── Virtuelle Kamera starten ──"
gst-launch-1.0 -q videotestsrc pattern=ball is-live=true \
! video/x-raw,format=YUY2,width=640,height=480,framerate=30/1 \
! pipewiresink mode=provide \
stream-properties="props,media.class=Video/Source,node.description=HPOS-Testkamera,node.name=hpos_kamera" \
>/tmp/hpos-kamera.log 2>&1 &
GST_PID=$!
sleep 4
if ! kill -0 "$GST_PID" 2>/dev/null; then
echo " ✗ GStreamer sofort beendet:"
tail -n 25 /tmp/hpos-kamera.log | sed 's/^/ /'
echo " Häufigste Ursache: gstreamer1.0-pipewire fehlt."
exit 1
fi
echo " ✓ GStreamer läuft (PID $GST_PID)"
# ── Nachweis, dass der Node existiert ────────────────────────────────────────
echo ""
echo "── Node-Nachweis ──"
if command -v wpctl >/dev/null 2>&1; then
wpctl status 2>&1 | sed 's/^/ /'
fi
if pw-cli ls Node 2>/dev/null | grep -qi 'hpos_kamera\|HPOS-Testkamera'; then
echo " ✓ Kamera-Node ist in PipeWire registriert"
else
echo " ! Node nicht eindeutig gefunden — vollständige Liste:"
pw-cli ls Node 2>/dev/null | grep -Ei 'node.name|media.class' | sed 's/^/ /'
fi
# ── Firefox ──────────────────────────────────────────────────────────────────
# Ohne media.webrtc.camera.allow-pipewire fragt Firefox weiterhin nur V4L2 ab und
# findet folgerichtig nichts. Die Einstellung wird hier vorab ins Profil geschrieben,
# damit der Test nicht an einem vergessenen Häkchen scheitert.
echo ""
echo "── Firefox vorbereiten ──"
if ! command -v firefox >/dev/null 2>&1 && ! command -v firefox-esr >/dev/null 2>&1; then
apt-get install -y --no-install-recommends firefox-esr >/dev/null 2>&1 \
|| echo " ! Firefox-Installation fehlgeschlagen"
fi
FF=$(command -v firefox || command -v firefox-esr || true)
if [ -n "$FF" ]; then
PROFIL=$(find "$HOME/.mozilla/firefox" -maxdepth 1 -name '*.default*' -type d 2>/dev/null | head -1)
if [ -z "$PROFIL" ]; then
timeout 25 "$FF" --headless --screenshot /tmp/ff-init.png about:blank >/dev/null 2>&1 || true
PROFIL=$(find "$HOME/.mozilla/firefox" -maxdepth 1 -name '*.default*' -type d 2>/dev/null | head -1)
fi
if [ -n "$PROFIL" ]; then
{
echo 'user_pref("media.webrtc.camera.allow-pipewire", true);'
echo 'user_pref("widget.use-xdg-desktop-portal.camera", 1);'
} >>"$PROFIL/user.js"
echo " ✓ Einstellungen ins Profil geschrieben: $PROFIL"
else
echo " ! Kein Firefox-Profil gefunden — bitte manuell in about:config setzen:"
echo " media.webrtc.camera.allow-pipewire = true"
fi
echo ""
echo " Starte Firefox mit dem Kameratest …"
"$FF" https://webcamtests.com/ >/tmp/hpos-firefox.log 2>&1 &
sleep 3
echo " → In der Termux:X11-App die Quelle 'HPOS-Testkamera' auswählen."
echo " Erwartet: ein springender Ball. Erscheint er, ist S3 bestanden."
else
echo " ! Ohne Firefox lässt sich der entscheidende Teil nicht prüfen."
fi
echo ""
echo " Beenden mit: kill $GST_PID ; pkill -f 'pipewire|wireplumber'"
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
# Läuft INNERHALB des Debian-Rootfs, aufgerufen von 04-s4-intent-handoff.sh.
#
# Richtet hpos-open als xdg-open-Handler für geo:, tel:, sms: und mailto: ein.
# Der Witz daran: nichts wird pro Anwendung gebaut. Adressbuch, Browser, Kalender
# und Dateimanager rufen alle xdg-open auf und landen dadurch automatisch hier.
set -uo pipefail
ZIEL="${HPOS_ZIEL:-Brandenburger Tor, Berlin}"
TOKEN_DATEI=/hpos/spike/out/bridge-token
echo "── Debian-Seite: Intent-Handoff ──"
export DEBIAN_FRONTEND=noninteractive
command -v xdg-open >/dev/null 2>&1 || apt-get install -y --no-install-recommends xdg-utils >/dev/null 2>&1
if [ ! -r "$TOKEN_DATEI" ]; then
echo " ✗ Token-Datei nicht lesbar: $TOKEN_DATEI"
echo " Läuft die Mini-Bridge? Sie erzeugt das Token beim ersten Start."
exit 1
fi
# ── hpos-open ────────────────────────────────────────────────────────────────
# Der Handler selbst: nimmt eine URI, schickt sie an die Bridge, fertig. Später
# ersetzt die Bridge-APK den Gegenpart, dieses Skript bleibt wie es ist.
install -d /usr/local/bin
cat >/usr/local/bin/hpos-open <<'HANDLER'
#!/bin/sh
# Reicht eine URI an die HPOS-Bridge auf der Android-Seite weiter.
# Aufruf: hpos-open "geo:0,0?q=Adresse"
set -eu
URI="${1:-}"
[ -n "$URI" ] || { echo "Aufruf: hpos-open <uri>" >&2; exit 2; }
TOKEN_DATEI="${HPOS_TOKEN_DATEI:-/hpos/spike/out/bridge-token}"
[ -r "$TOKEN_DATEI" ] || { echo "hpos-open: kein Token unter $TOKEN_DATEI" >&2; exit 1; }
TOKEN="$(cat "$TOKEN_DATEI")"
printf '%s\t%s\n' "$TOKEN" "$URI" | timeout 20 python3 -c '
import socket, sys
try:
s = socket.create_connection(("127.0.0.1", 8474), 10)
except OSError as e:
sys.exit(f"hpos-open: Bridge nicht erreichbar ({e})")
s.sendall(sys.stdin.buffer.read())
s.shutdown(socket.SHUT_WR)
antwort = s.recv(4096).decode().strip()
print(antwort)
sys.exit(0 if antwort.startswith("OK") else 1)
'
HANDLER
chmod 755 /usr/local/bin/hpos-open
echo " ✓ /usr/local/bin/hpos-open eingerichtet"
# ── Als Standardanwendung registrieren ───────────────────────────────────────
cat >/usr/share/applications/hpos-open.desktop <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=Auf dem Telefon öffnen
Comment=Reicht Adressen, Rufnummern und Nachrichten an die Android-Seite weiter
Exec=hpos-open %u
Terminal=false
NoDisplay=true
MimeType=x-scheme-handler/geo;x-scheme-handler/tel;x-scheme-handler/sms;x-scheme-handler/smsto;x-scheme-handler/mailto;
DESKTOP
update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
for schema in geo tel sms smsto; do
xdg-mime default hpos-open.desktop "x-scheme-handler/$schema" 2>/dev/null \
&& echo "$schema: → hpos-open" \
|| echo " ! $schema: Registrierung fehlgeschlagen"
done
echo " Zugeordnet laut System:"
for schema in geo tel sms; do
printf ' %-6s %s\n' "$schema:" "$(xdg-mime query default "x-scheme-handler/$schema" 2>/dev/null || echo '?')"
done
# ── Der Beweis ───────────────────────────────────────────────────────────────
echo ""
echo "── Test 1: hpos-open direkt ──"
KODIERT=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))' "$ZIEL")
if hpos-open "geo:0,0?q=$KODIERT"; then
echo " ✓ Bridge hat angenommen"
else
echo " ✗ Bridge hat abgelehnt oder war nicht erreichbar"
fi
sleep 5
echo ""
echo "── Test 2: über xdg-open ──"
echo " Das ist der eigentliche Beweis: der Weg, den jede Linux-Anwendung nimmt."
if xdg-open "geo:0,0?q=$KODIERT" 2>&1; then
echo " ✓ xdg-open hat den Handler aufgerufen"
else
echo " ✗ xdg-open ist gescheitert"
fi
sleep 5
echo ""
echo "── Test 3: Rufnummer ──"
hpos-open "tel:+4930901820" || echo " ! tel: fehlgeschlagen"
echo ""
echo " Jetzt aufs Telefon schauen: läuft die Navigation, liegt das Overlay darüber?"
echo " Zurück ins Linux über den Android-Task-Manager."
Executable
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# Gemeinsame Hilfsfunktionen für die Phase-0-Spike-Skripte.
# Wird von den 0x-Skripten eingebunden, nicht direkt ausgeführt.
# Farben nur wenn wir auf einem Terminal landen — sonst verschmutzen sie das Protokoll.
if [ -t 1 ]; then
C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_ERR=$'\033[31m'
C_HEAD=$'\033[1;36m'; C_DIM=$'\033[2m'; C_OFF=$'\033[0m'
else
C_OK=""; C_WARN=""; C_ERR=""; C_HEAD=""; C_DIM=""; C_OFF=""
fi
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT_DIR="$REPO_ROOT/spike/out"
mkdir -p "$OUT_DIR"
# Jeder Lauf bekommt sein eigenes Protokoll. Die Sammlung ist später die halbe
# Auswertung — deshalb wird alles mitgeschrieben, auch was funktioniert hat.
LOG="${LOG:-$OUT_DIR/$(basename "${0%.sh}").log}"
log() { printf '%s\n' "$*" | tee -a "$LOG"; }
section() { log ""; log "${C_HEAD}══ $* ══${C_OFF}"; }
ok() { log "${C_OK}${C_OFF} $*"; }
warn() { log "${C_WARN} !${C_OFF} $*"; }
fail() { log "${C_ERR}${C_OFF} $*"; }
hint() { log "${C_DIM} $*${C_OFF}"; }
# Befehl ausführen, Ausgabe ins Protokoll, Fehler nicht tödlich.
# Für alles, was scheitern DARF, ohne den Spike abzubrechen.
try() {
log "${C_DIM} \$ $*${C_OFF}"
if "$@" >>"$LOG" 2>&1; then
return 0
else
local rc=$?
warn "fehlgeschlagen (Code $rc): $*"
return $rc
fi
}
# Wie try(), aber ein Fehlschlag beendet den Spike. Für echte Voraussetzungen.
need() {
log "${C_DIM} \$ $*${C_OFF}"
if ! "$@" >>"$LOG" 2>&1; then
fail "Abbruch — notwendiger Schritt fehlgeschlagen: $*"
log ""
log "Vollständiges Protokoll: $LOG"
exit 1
fi
}
have() { command -v "$1" >/dev/null 2>&1; }
in_termux() {
case "${PREFIX:-}" in
*com.termux*) [ -d "$PREFIX/bin" ] ;;
*) return 1 ;;
esac
}
require_termux() {
if ! in_termux; then
fail "Dieses Skript läuft in Termux auf dem Telefon, nicht auf dem PC."
exit 1
fi
}
# Fragt am Ende jedes Beweises nach dem, was nur ein Mensch beurteilen kann:
# ob es tatsächlich funktioniert hat. Das Ergebnis geht ins Protokoll.
verdict() {
local beweis="$1" frage="$2"
log ""
log "${C_HEAD}── Beurteilung $beweis ──${C_OFF}"
log "$frage"
printf ' [j] ja [n] nein [t] teilweise > '
read -r antwort </dev/tty || antwort="?"
local text
case "$antwort" in
j|J) text="BESTANDEN" ;;
n|N) text="GESCHEITERT" ;;
t|T) text="TEILWEISE" ;;
*) text="UNKLAR" ;;
esac
printf ' Anmerkung (eine Zeile, Enter für nichts) > '
read -r notiz </dev/tty || notiz=""
log " Ergebnis $beweis: $text ${notiz:+— $notiz}"
printf '%s\t%s\t%s\n' "$beweis" "$text" "$notiz" >>"$OUT_DIR/ergebnisse.tsv"
log ""
log "Ins Protokoll übertragen: docs/spike-protokoll.md"
log "Rohdaten: $LOG"
}
# Gerätesteckbrief — landet in jedem Protokoll, damit Meldungen vergleichbar sind.
geraete_steckbrief() {
section "Gerät"
local p
for p in ro.product.manufacturer ro.product.model ro.product.device \
ro.build.version.release ro.build.version.sdk ro.board.platform \
ro.hardware ro.soc.model; do
log " $p = $(getprop "$p" 2>/dev/null || echo '?')"
done
log " Arch = $(uname -m)"
log " Kernel = $(uname -r)"
if have free; then log " RAM = $(free -h 2>/dev/null | awk '/Mem:/{print $2}')"; fi
}