
phone-deck v3.0.1
Console de contrôle auto-hébergée et bureau à distance pour postes de travail Linux via Tailscale, avec streaming WebRTC, contrôle vocal avec LLM local, macros de scène, entrée virtuelle, transfert de fichiers et télémétrie système en direct.
phone-deck
Transformez un téléphone Android de rechange (ou tout appareil avec un navigateur) en un panneau de contrôle auto-hébergé et en bureau à distance pour votre station de travail Linux — via votre réseau privé Tailscale, sans applications tierces.
Tout a commencé par « que faire d'un vieux téléphone ? » et est devenu un plan de contrôle pour une configuration Hyprland : contrôle des fenêtres/espaces de travail, macros de scène, routeur vocal push-to-talk avec un assistant LLM local, contrôles contextuels dans l'application, streaming audio + écran bidirectionnel, un bureau à distance tactile, un clavier/souris virtuel, transfert de fichiers + partage téléphone→PC, et télémétrie en direct — le tout servi comme une PWA installée avec un aspect terminal phosphor.
Note sur la portabilité. Ceci a été construit pour une configuration spécifique : Linux basé sur Arch (Garuda) + Hyprland + PipeWire + NVIDIA + Tailscale, avec les dotfiles end-4 / illogical-impulse. L'architecture est générique, mais plusieurs intégrations sont spécifiques à l'environnement (noms de moniteurs, liaisons d'espaces de travail, chemin du thème matugen,
cpupower/nvidia-smi). Considérez-le comme une référence de travail à adapter, pas un package clé en main.
Fonctionnalités
L'UI est une application web sombre, paysage, à onglets (synchronisée avec le thème de votre bureau) :
| Onglet | Ce qu'il fait |
|---|---|
| Espaces | Grille d'espaces de travail en direct par écran (taper pour changer), liste des fenêtres en direct (taper pour focaliser), bascule DPMS par écran + luminosité ddcutil |
| Distant | Pavé tactile à l'écran (glisser/taper/deux doigts) + clavier virtuel avec modificateurs et combinaisons, via un périphérique uinput du noyau |
| Modes | Macros de scène en un tap — lancer/fermer/agencer des dispositions d'applications complètes sur plusieurs écrans (ex. « Travail » / « Loisirs ») |
| Voix | Routeur vocal push-to-talk (faster-whisper, CPU) : voies de mot-clé — macro (exécuter un mode/une commande), type (dicter), input (phrase → combinaison de touches) — avec confirmation avant exécution, plus « friday », un répondeur LLM local en lecture seule (Ollama + Qwen) avec recherche web auto-hébergée et connaissance de l'état système en direct |
| Audio | Sourdine micro/haut-parleur, volume, sélecteurs de périphériques de sortie et d'entrée, transport playerctl + couverture d'album (suit le lecteur MPRIS actif) |
| Stream | Audio WebRTC bidirectionnel (PC↔téléphone, avec micro du téléphone et sortie téléphone uniquement) + vidéo d'écran, et taper pour contrôler l'écran streamé = un véritable bureau à distance |
| Système | Bascule du mode de performance, changement de thème en direct, statut Tailscale, principaux processus (taper pour tuer), verrouillage, redémarrage de NetworkManager, mise en veille/redémarrage/extinction |
| Fichiers | Capture d'écran d'un moniteur → visualisation/téléchargement sur le téléphone ; déposer un fichier téléphone→PC |
| Config | Modifier la configuration commands.json / modes.json (et voix/contexte) depuis le téléphone (validé JSON) |
Toujours présent, au-dessus des onglets — une bande contextuelle qui affiche ce qui se passe en ce moment : un appel actif (sourdine / aller à), média en cours de lecture + transport, et contrôles de clavier dans l'application pour l'application focalisée (YouTube / Brave / Teams), fournis sans voler le focus.
Deux autres, au-delà des onglets :
- Envoyer au Rig — la PWA s'enregistre comme cible de partage Android : partagez une image, du texte ou un lien depuis n'importe quelle application de votre téléphone et ils atterrissent sur le rig — les images sauvegardées dans un répertoire drop et placées dans le presse-papier sous forme de PNG prêt à coller, le texte dans le presse-papier, un lien nu ouvert.
- Cogitateur Ambiant — après quelques minutes d'inactivité, le panneau devient un tableau de bord phosphor : télémétrie à cadran avec maintien de pic (« était-ce au maximum pendant mon absence ? »), horloge, lecture en cours.
Plus : connexion par code PIN numérique, verrouillage de l'écran, PWA plein écran paysage, et verrouillage par force brute sur la connexion.
Architecture
téléphone / ordinateur portable (PWA navigateur)
│ HTTPS + WSS (Tailscale uniquement)
▼
tailscale serve ──► Application web FastAPI ── Socket Unix ──► deckd
(certificat TLS réel) (exécutée en tant que votre utilisateur) (noms d'actions) (exécutée en tant que root)
│ │
hyprctl · pactl · ddcutil · grim · liste d'autorisation fixe
wf-recorder · uinput · WebRTC de commandes privilégiées
(cpupower, nvidia-smi,
systemctl, …)
Deux processus :
app/— l'application web s'exécute en tant que votre utilisateur normal. Elle fait tout ce qui ne nécessite pas root : contrôle Hyprland, audio, luminosité, capture d'écran, entrée virtuelle, streaming WebRTC, transfert de fichiers.deckd/— un petit assistant root (stdlib uniquement, aucune dépendance) pour les quelques actions privilégiées. L'application web ne lui envoie jamais de chaînes shell — seulement des noms d'actions d'une liste d'autorisation fixe (governor_performance,gpu_power_limit,suspend, …), validée dansdeckd/actions.pyavant toute exécution. Même si l'application web était complètement compromise, le rayon d'impact est exactement les actions autorisées, sans injection d'arguments. Le socket estroot:<groupe>mode0660.
Modèle de sécurité
- Accessible uniquement à l'intérieur de votre tailnet —
tailscale servel'expose sur votre nom MagicDNS avec un vrai certificat Let's Encrypt ; il ne se lie jamais à0.0.0.0. - Connexion JWT (PIN → cookie signé), avec verrouillage à backoff exponentiel après échecs répétés.
- Les actions privilégiées sont isolées derrière le démon assistant autorisé.
- Les téléchargements sont nettoyés de leur nom de base (impossible de sortir du répertoire drop).
L'application web peut exécuter vos commandes shell configurées et injecter des entrées en tant que votre utilisateur — c'est, par conception, une télécommande pour votre machine. Gardez-la sur votre tailnet, derrière le PIN, et ne l'exposez pas publiquement.
Prérequis
- Linux avec Hyprland (wlroots), PipeWire (avec compatibilité
pactl/PulseAudio) - Python ≥ 3.11 et uv
- Tailscale (avec certificats HTTPS activés pour votre tailnet)
- Outils CLI utilisés par diverses fonctionnalités (installez ce que vous voulez utiliser) :
hyprctl,pactl/pw-record/pw-play,playerctl,ddcutil,wf-recorder,grim,cpupower,nvidia-smi,kitty(ou votre terminal) - Votre utilisateur dans le groupe
input(pour/dev/uinput) et le groupei2c(pourddcutil) aiortc+ PyAV (installés viauv) pour le streaming audio/vidéo
Installation
git clone <votre-url-de-repo> phone-deck
cd phone-deck
uv sync # crée .venv et installe les dépendances
uv run python -m app.set_pin # définissez votre PIN de déverrouillage
1. L'assistant root (deckd)
Modifiez systemd/deckd.service d'abord — définissez les chemins et DECK_SOCKET_GROUP à un groupe auquel votre utilisateur appartient (généralement votre groupe principal) :
sudo cp systemd/deckd.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now deckd
deckd utilise le Python système (stdlib uniquement — pas besoin de venv).
2. L'application web
Exécutez-la directement pour le développement :
uv run uvicorn app.main:app --host 127.0.0.1 --port 8765
…ou comme service utilisateur. Modifiez d'abord les chemins dans systemd/phone-deck.service, puis installez-le.
Important : sur une configuration où Hyprland n'est pas lancé via uwsm, le graphical-session.target de systemd ne s'active jamais, donc l'unité utilisateur ne démarrera pas automatiquement à la connexion. La solution fiable est de le lancer depuis Hyprland avec l'environnement de session importé — ajoutez à votre démarrage automatique Hyprland (par exemple, le ~/.config/hypr/custom/execs.conf de end-4) :
exec-once = systemctl --user import-environment WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS XDG_CURRENT_DESKTOP && systemctl --user start phone-deck
3. HTTPS via Tailscale
-
Activez les certificats une fois : console d'administration Tailscale → DNS → Enable HTTPS Certificates.
-
Faites front-end de l'application avec un vrai certificat :
sudo tailscale serve --bg 127.0.0.1:8765 -
Définissez
DECK_SECURE_COOKIES=1dans l'environnement de l'application web une fois HTTPS en place.
4. Installer sur le téléphone
Ouvrez https://<votre-hôte>.<votre-tailnet>.ts.net/ dans Chrome sur le téléphone (il doit être sur votre tailnet), connectez-vous, puis ⋮ → Installer l'application. Elle se lance en plein écran, verrouillé en paysage, avec l'écran maintenu éveillé. HTTPS est requis pour le service worker PWA + wake-lock.
Fonctionne depuis n'importe quel appareil du tailnet — ordinateurs portables inclus (les surfaces tactiles utilisent les Pointer Events, donc une souris fonctionne aussi). Le flux WebRTC est à un seul pair (un seul spectateur à la fois).
Configuration
Toute la configuration d'exécution se trouve dans ~/.config/phone-deck/ et est modifiable depuis l'onglet Config (validé JSON, conserve un .bak). Les changements prennent effet à la prochaine action — pas de redémarrage.
commands.json — actions shell personnalisées
[
{ "id": "reload_wm", "label": "Recharger WM", "run": "hyprctl reload" },
{ "id": "deploy", "label": "Déployer le site", "run": "cd /srv/app && ./deploy.sh",
"confirm": true, "timeout": 120 }
]
run peut être une chaîne (exécutée via bash -lc) ou une liste (argv, sans shell). Ces commandes s'exécutent en tant que votre utilisateur — c'est un fichier local de confiance.
modes.json — macros de scène
Chaque mode est une liste ordonnée d'étapes exécutées via hyprctl :
launch {cmd, workspace, match?, once?}— ouvrir une application sur un espace de travail. Avecmatch(classe de fenêtre) +once, elle ne dupliquera pas, et elle replace la fenêtre si elle atterrit sur le mauvais espace de travail (nécessaire pour les applications comme les PWA de navigateur dont la fenêtre provient d'un processus existant).close {match, workspaces?}— fermer les fenêtres d'une classe, optionnellement seulement sur les espaces de travail listés.move {match, workspace}— déplacer les fenêtres correspondantes vers un espace de travail.focus {workspace}— amener un espace de travail à l'affichage (par exemple, réinitialiser chaque moniteur à son espace de travail d'origine).
Liaison espace de travail Hyprland → moniteur
Le placement des modes est déterministe uniquement si vos espaces de travail sont épinglés aux moniteurs. Ajoutez des liaisons à votre configuration Hyprland (ajustez les numéros d'espace de travail et les noms de moniteurs aux vôtres) :
workspace = 1, monitor:DP-3, default:true
workspace = 2, monitor:DP-3
# … etc
Thème
Le panneau arbore un skin phosphor-terminal avec quatre profils de couleur commutables en direct — vert (par défaut), ambre, glace, et auto (dérive la teinte phosphor de votre palette de fond d'écran matugen, ~/.local/state/quickshell/user/generated/colors.json). Basculez dans l'onglet Système ; le choix persiste dans ~/.config/phone-deck/theme.json. app/theme.py le sert comme /theme.css. Le délai d'inactivité du Cogitateur Ambiant et sa ligne de liturgie sont également configurables ici.
Comment fonctionne le streaming
WebRTC bidirectionnel via aiortc + PyAV, ponté vers PipeWire/Hyprland avec des pipes de sous-processus (évite les liaisons fragiles d'E/S de périphériques) :
- PC → audio téléphone : capturer le
.monitordu sink par défaut avecparec→ Opus. - Téléphone → micro PC : recevoir le micro du téléphone → un sink null PipeWire dont les applications de moniteur sélectionnent comme entrée (« Monitor of PhoneDeckMic »).
- Sortie téléphone uniquement : rediriger la lecture PC vers un sink virtuel pour que les haut-parleurs deviennent silencieux pendant que vous écoutez sur le téléphone ; restauré à l'arrêt.
- PC → vidéo téléphone :
wf-recordercapture un moniteur → aiortc, avec des préréglages de qualité (540p / 720p / 1080p) pour maintenir un débit binaire raisonnable sur des liaisons instables. - Bureau à distance tactile : taper/glisser sur la vidéo streamée → positionnement absolu du curseur (
hyprctl movecursor) + clicsuinput. Taper = clic, glisser = déplacer, appui long = clic droit.
Contraintes : latence d'environ 150–300 ms ; fiable uniquement tant que la PWA est au premier plan (Android suspend les onglets en arrière-plan) ; utilisez un casque pour éviter l'écho ; un seul pair de streaming à la fois ; un navigateur ne peut pas capturer l'audio de sa propre application (micro uniquement).
Stack technique
- Backend : FastAPI + Uvicorn, WebSockets, aiortc/PyAV, python-evdev, psutil, PyJWT
- Frontend : HTML + Tailwind (CDN) + vanilla JS, PWA (manifest + service worker + wake-lock)
- Démon assistant : bibliothèque standard Python uniquement
- Transport : Tailscale (
tailscale servepour HTTPS/WSS)
Tests
uv run pytest
Couvre la liste d'autorisation de deckd, JWT + verrouillage de connexion, hachage du PIN, validation de l'éditeur de configuration, analyse des modes, logique de qualité/commande du streaming, et la carte des touches d'entrée. Les chemins WebRTC/PipeWire/Hyprland en direct sont validés par boucle de retour pendant le développement.
Structure du projet
app/ Application web FastAPI
main.py routes + WebSockets
hypr.py Hyprland (instantané, dispatch, socket d'événements, curseur)
audio.py Contrôle des périphériques PipeWire + playerctl
audio_rtc.py Pont audio WebRTC + vidéo d'écran
hid.py Clavier/souris virtuel via uinput
modes.py Moteur de macros de scène
brightness.py ddcutil
grab.py Capture d'écran + téléchargement de fichier
theme.py matugen → CSS
...
deckd/ Assistant root (stdlib uniquement) + liste d'autorisation d'actions
systemd/ Unités de service
tests/ Suite pytest
Licence
MIT — voir LICENSE (ajoutez-en une avant de publier si vous souhaitez une licence différente).
Remerciements
Construit pour Hyprland, PipeWire, Tailscale, aiortc, et les dotfiles end-4.