
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](https://github.com/end-4/dots-hyprland). 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 dans [`deckd/actions.py`](https://github.com/smit-shah-gg/phone-deck/blob/main/deckd/actions.py) avant 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 est `root:<groupe>` mode `0660`.
### Modèle de sécurité
- Accessible **uniquement à l'intérieur de votre tailnet** — `tailscale serve` l'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](https://docs.astral.sh/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 groupe **`i2c`** (pour `ddcutil`)
- `aiortc` + PyAV (installés via `uv`) pour le streaming audio/vidéo
---
## Installation
```bash
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) :
```bash
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 :
```bash
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) :
```bash
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
1. Activez les certificats une fois : console d'administration Tailscale → **DNS → Enable HTTPS Certificates**.
2. Faites front-end de l'application avec un vrai certificat :
```bash
sudo tailscale serve --bg 127.0.0.1:8765
```
3. Définissez `DECK_SECURE_COOKIES=1` dans 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
```json
[
{ "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. Avec `match` (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 `.monitor` du sink par défaut avec `parec` → 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-recorder` capture 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`) + clics `uinput`. 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 serve` pour HTTPS/WSS)
## Tests
```bash
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](https://hyprland.org/), [PipeWire](https://pipewire.org/),
[Tailscale](https://tailscale.com/), [aiortc](https://github.com/aiortc/aiortc), et les
[dotfiles end-4](https://github.com/end-4/dots-hyprland).