
Plataforma de control autoalojada y escritorio remoto para estaciones de trabajo Linux a través de Tailscale, con transmisión WebRTC, control por voz con LLM local, macros de escena, entrada virtual, transferencia de archivos y telemetría del sistema en vivo.
# phone-deck
Convierte un teléfono Android de repuesto (o cualquier dispositivo con un navegador) en una consola de control autogestionada y escritorio remoto para tu estación de trabajo Linux, a través de tu red privada de Tailscale, sin aplicaciones de terceros.
Comenzó como "¿qué hago con un teléfono viejo?" y se convirtió en un plano de control para un rig Hyprland: control de ventanas/espacios de trabajo, macros de escena, un enrutador de voz push-to-talk con un asistente LLM local, controles contextuales dentro de las aplicaciones, transmisión bidireccional de audio + pantalla, un escritorio remoto táctil, un teclado/ratón virtual, transferencia de archivos + uso compartido teléfono→PC, y telemetría en vivo, todo servido como un PWA instalado con apariencia de terminal de fósforo.
> **Nota sobre portabilidad.** Esto fue construido para una configuración específica: **Linux basado en Arch (Garuda) + Hyprland + PipeWire + NVIDIA + Tailscale**, con los dotfiles [end-4 / illogical-impulse](https://github.com/end-4/dots-hyprland). La arquitectura es genérica, pero varias integraciones son específicas del entorno (nombres de monitores, enlaces de espacios de trabajo, ruta del tema matugen, `cpupower`/`nvidia-smi`). Tómalo como una referencia funcional para adaptar, no como un paquete listo para usar.
---
## Características
La interfaz es una aplicación web oscura, con pestañas y apaisada (sincronizada con el tema de tu escritorio):
| Pestaña | Qué hace |
|---|---|
| **Workspaces** | Cuadrícula de espacios de trabajo en vivo por monitor (toca para cambiar), lista de ventanas en vivo (toca para enfocar), alternancia DPMS por monitor + brillo con `ddcutil` |
| **Remote** | Trackpad en pantalla (arrastrar/tocar/dos dedos) + teclado virtual con modificadores y combinaciones, a través de un dispositivo `uinput` del kernel |
| **Modes** | Macros de escena con un toque: lanzar/cerrar/organizar diseños completos de aplicaciones en todos los monitores (p. ej., "Trabajo" / "Ocio") |
| **Voice** | Enrutador de voz push-to-talk (faster-whisper, CPU): canales de palabra clave — `macro` (ejecutar un modo/comando), `type` (dictado), `input` (frase → combinación de teclas) — con **confirmar antes de ejecutar**, además de **"friday"**, un respondedor LLM local de solo lectura (Ollama + Qwen) con búsqueda web autogestionada y conocimiento del estado del sistema en vivo |
| **Audio** | Silenciar micrófono/altavoz, volumen, selectores de dispositivos de salida **y** entrada, transporte `playerctl` + carátula (sigue el reproductor MPRIS activo) |
| **Stream** | **Audio WebRTC bidireccional** (PC↔teléfono, con el teléfono como micrófono y salida solo para el teléfono) **+ vídeo de pantalla**, y **toca para controlar la pantalla transmitida** = un verdadero escritorio remoto |
| **System** | Alternancia del modo de rendimiento, cambio de **tema** en vivo, estado de Tailscale, procesos principales (toca para matar), bloqueo, reinicio de NetworkManager, suspender/reiniciar/apagar |
| **Files** | Captura de pantalla de un monitor → ver/descargar en el teléfono; soltar un archivo del teléfono al PC |
| **Config** | Editar los archivos de configuración `commands.json` / `modes.json` (y contexto/voz) desde el teléfono (validación JSON) |
Siempre visible, sobre las pestañas: una **barra de contexto** que muestra lo que está sucediendo ahora mismo: una llamada activa (silenciar / saltar a ella), multimedia en reproducción + transporte, y controles de teclado en la aplicación para la aplicación enfocada (YouTube / Brave / Teams), entregados **sin robar el foco**.
Dos más, fuera de las pestañas:
- **Send-to-Rig** — el PWA se registra como un **destino de uso compartido** de Android: comparte una imagen, texto o un enlace desde cualquier aplicación de tu teléfono y aterriza en el rig — las imágenes se guardan en un directorio de destino *y* se colocan en el portapapeles como PNG listo para pegar, el texto en el portapapeles, un enlace desnudo se abre.
- **Ambient Cogitator** — después de unos minutos de inactividad, la consola se convierte en un panel de instrumentos de fósforo: telemetría con aguja indicadora y retención de pico ("¿estuvo al máximo mientras no estaba?"), reloj, ahora en reproducción.
Además: inicio de sesión con PIN de teclado numérico en pantalla, bloqueo de pantalla, PWA a pantalla completa apaisada, y bloqueo por fuerza bruta en el inicio de sesión.
---
## Arquitectura
```
teléfono / portátil (navegador PWA)
│ HTTPS + WSS (solo Tailscale)
▼
tailscale serve ──► Aplicación web FastAPI ── Socket Unix ──► deckd
(certificado TLS real) (se ejecuta como tu usuario) (nombres de acción) (se ejecuta como root)
│ │
hyprctl · pactl · ddcutil · grim · lista blanca fija de
wf-recorder · uinput · WebRTC comandos privilegiados
(cpupower, nvidia-smi,
systemctl, …)
```
Dos procesos:
- **`app/` — la aplicación web** se ejecuta como tu usuario normal. Hace todo lo que no necesita root: control de Hyprland, audio, brillo, captura de pantalla, entrada virtual, transmisión WebRTC, transferencia de archivos.
- **`deckd/` — un pequeño ayudante root** (solo stdlib, sin dependencias) para las pocas acciones privilegiadas. La aplicación web nunca le envía cadenas de shell, solo **nombres de acción** de una lista blanca fija (`governor_performance`, `gpu_power_limit`, `suspend`, …), validados en [`deckd/actions.py`](https://github.com/smit-shah-gg/phone-deck/blob/main/deckd/actions.py) antes de ejecutar cualquier cosa. Incluso si la aplicación web estuviera completamente comprometida, el radio de explosión son exactamente las acciones de la lista blanca, sin inyección de argumentos. El socket es `root:<grupo>` modo `0660`.
### Modelo de seguridad
- Se accede **solo dentro de tu tailnet** — `tailscale serve` lo expone en tu nombre MagicDNS con un certificado real de Let's Encrypt; nunca se vincula a `0.0.0.0`.
- Inicio de sesión **JWT** (PIN → cookie firmada), con bloqueo de retroceso exponencial después de fallos repetidos.
- Las acciones privilegiadas están aisladas detrás del demonio auxiliar de lista blanca.
- Las subidas tienen el nombre base saneado (no pueden escapar del directorio de destino).
> La aplicación web *puede* ejecutar tus comandos de shell configurados e inyectar entrada como tu usuario — es, por diseño, un control remoto para tu máquina. Mantenla en tu tailnet, detrás del PIN, y no la expongas públicamente.
---
## Requisitos
- Linux con **Hyprland** (wlroots), **PipeWire** (con compatibilidad `pactl`/PulseAudio)
- **Python ≥ 3.11** y **[uv](https://docs.astral.sh/uv/)**
- **Tailscale** (con certificados HTTPS habilitados para tu tailnet)
- Herramientas CLI utilizadas por varias características (instala las que quieras usar):
`hyprctl`, `pactl` / `pw-record` / `pw-play`, `playerctl`, `ddcutil`,
`wf-recorder`, `grim`, `cpupower`, `nvidia-smi`, `kitty` (o tu terminal)
- Tu usuario en el grupo **`input`** (para `/dev/uinput`) y **`i2c`** (para `ddcutil`)
- `aiortc` + PyAV (instalado vía `uv`) para transmisión de audio/vídeo
---
## Instalación
```bash
git clone <tu-url-del-repositorio> phone-deck
cd phone-deck
uv sync # crea .venv e instala dependencias
uv run python -m app.set_pin # establece tu PIN de desbloqueo
```
### 1. El ayudante root (`deckd`)
Primero edita `systemd/deckd.service` — establece las rutas y `DECK_SOCKET_GROUP` a un grupo al que pertenezca tu usuario (normalmente tu grupo principal):
```bash
sudo cp systemd/deckd.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now deckd
```
`deckd` ejecuta el Python del sistema (solo stdlib — no necesita venv).
### 2. La aplicación web
Ejecútala directamente para desarrollo:
```bash
uv run uvicorn app.main:app --host 127.0.0.1 --port 8765
```
…o como un servicio de usuario. Primero edita las rutas de `systemd/phone-deck.service`, luego instálalo.
**Importante:** en una configuración donde Hyprland *no* se inicia a través de uwsm, el `graphical-session.target` de systemd nunca se activa, por lo que la unidad de usuario no se iniciará automáticamente al iniciar sesión. La solución confiable es iniciarlo desde Hyprland con el entorno de sesión importado — agrega a tu inicio automático de Hyprland (p. ej., `~/.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 a través de Tailscale
1. Habilita los certificados una vez: Consola de administración de Tailscale → **DNS → Enable HTTPS Certificates**.
2. Pon la aplicación detrás de un certificado real:
```bash
sudo tailscale serve --bg 127.0.0.1:8765
```
3. Establece `DECK_SECURE_COOKIES=1` en el entorno de la aplicación web una vez que HTTPS esté activo.
### 4. Instalar en el teléfono
Abre `https://<tu-host>.<tu-tailnet>.ts.net/` en Chrome en el teléfono (debe estar en tu tailnet), inicia sesión, luego **⋮ → Instalar aplicación**. Se inicia a pantalla completa, bloqueado en apaisado, con la pantalla activa. HTTPS es necesario para el service worker del PWA + wake-lock.
> Funciona desde cualquier dispositivo del tailnet, incluidos portátiles (las superficies táctiles usan Pointer Events, por lo que un ratón también funciona). La transmisión WebRTC es de un solo par (un visor a la vez).
---
## Configuración
Toda la configuración en tiempo de ejecución reside en `~/.config/phone-deck/` y se puede editar desde la pestaña **Config** (validación JSON, guarda un `.bak`). Los cambios surten efecto en la siguiente acción — sin necesidad de reiniciar.
### `commands.json` — acciones de shell personalizadas
```json
[
{ "id": "reload_wm", "label": "Recargar WM", "run": "hyprctl reload" },
{ "id": "deploy", "label": "Desplegar sitio", "run": "cd /srv/app && ./deploy.sh",
"confirm": true, "timeout": 120 }
]
```
`run` puede ser una cadena (ejecutada vía `bash -lc`) o una lista (argv, sin shell). Se ejecutan como tu usuario — es un archivo local de confianza.
### `modes.json` — macros de escena
Cada modo es una lista ordenada de pasos ejecutados vía `hyprctl`:
- `launch {cmd, workspace, match?, once?}` — abre una aplicación en un espacio de trabajo. Con `match` (clase de ventana) + `once`, no se duplica, y reubica la ventana si esta cae en el espacio de trabajo incorrecto (necesario para aplicaciones como PWAs del navegador cuya ventana proviene de un proceso existente).
- `close {match, workspaces?}` — cierra ventanas de una clase, opcionalmente solo en los espacios de trabajo listados.
- `move {match, workspace}` — mueve las ventanas coincidentes a un espacio de trabajo.
- `focus {workspace}` — cambia un espacio de trabajo a la vista (p. ej., restablecer cada monitor a su espacio de trabajo principal).
### Enlace espacio de trabajo de Hyprland → monitor
La ubicación del modo es determinista solo si tus espacios de trabajo están vinculados a monitores. Agrega enlaces a tu configuración de Hyprland (ajusta los números de espacio de trabajo y nombres de monitor a los tuyos):
```
workspace = 1, monitor:DP-3, default:true
workspace = 2, monitor:DP-3
# … etc
```
### Tema
La consola tiene una apariencia de **terminal de fósforo** con cuatro perfiles de color intercambiables en vivo: **verde** (predeterminado), **ámbar**, **hielo** y **auto** (deriva el tono de fósforo de la paleta de fondos de pantalla de matugen, `~/.local/state/quickshell/user/generated/colors.json`). Cámbialo en la pestaña **System**; la elección persiste en `~/.config/phone-deck/theme.json`. `app/theme.py` lo sirve como `/theme.css`. El tiempo de espera de inactividad del Ambient Cogitator y la línea de liturgia también se pueden configurar aquí.
---
## Cómo funciona la transmisión
WebRTC bidireccional a través de **aiortc + PyAV**, conectado a PipeWire/Hyprland con tuberías de subprocesos (evita enlaces frágiles de dispositivos de E/S):
- **PC → audio del teléfono:** captura el `.monitor` del sink predeterminado con `parec` → Opus.
- **Teléfono → micrófono del PC:** recibe el micrófono del teléfono → un sink nulo de PipeWire cuyas aplicaciones de monitor seleccionan como entrada ("Monitor of PhoneDeckMic").
- **Salida solo para el teléfono:** enruta la reproducción del PC a un sink virtual para que los altavoces se silencien mientras escuchas en el teléfono; se restaura al detener.
- **PC → vídeo del teléfono:** `wf-recorder` captura un monitor → aiortc, con preajustes de calidad (540p / 720p / 1080p) para mantener la tasa de bits razonable en enlaces inestables.
- **Escritorio remoto táctil:** tocar/arrastrar en el vídeo transmitido → posicionamiento absoluto del cursor (`hyprctl movecursor`) + clics `uinput`. Tocar = clic, arrastrar = mover, pulsación larga = clic derecho.
Limitaciones: latencia de ~150–300 ms; confiable solo mientras el PWA esté en primer plano (Android suspende las pestañas en segundo plano); usa auriculares para evitar eco; un solo par de transmisión a la vez; un navegador no puede capturar el audio de la *propia* aplicación del teléfono (solo micrófono).
---
## Pila tecnológica
- **Backend:** FastAPI + Uvicorn, WebSockets, aiortc/PyAV, python-evdev, psutil, PyJWT
- **Frontend:** HTML + Tailwind (CDN) + vanilla JS, PWA (manifest + service worker + wake-lock)
- **Demonio auxiliar:** solo biblioteca estándar de Python
- **Transporte:** Tailscale (`tailscale serve` para HTTPS/WSS)
## Pruebas
```bash
uv run pytest
```
Cubre la lista blanca de `deckd`, JWT + bloqueo de inicio de sesión, hashing de PIN, validación del editor de configuración, análisis de modos, calidad de transmisión/lógica de comandos y el mapa de teclas de entrada. Las rutas en vivo de WebRTC/PipeWire/Hyprland se validan mediante bucle de retroceso durante el desarrollo.
## Estructura del proyecto
```
app/ Aplicación web FastAPI
main.py rutas + WebSockets
hypr.py Hyprland (instantánea, dispatch, socket de eventos, cursor)
audio.py Control de dispositivos PipeWire + playerctl
audio_rtc.py Puente WebRTC de audio + vídeo de pantalla
hid.py teclado/ratón virtual vía uinput
modes.py motor de macros de escena
brightness.py ddcutil
grab.py captura de pantalla + subida de archivos
theme.py matugen → CSS
...
deckd/ ayudante root (solo stdlib) + lista blanca de acciones
systemd/ unidades de servicio
tests/ suite de pruebas
```
## Licencia
MIT — consulta `LICENSE` (añade uno antes de publicar si deseas una licencia diferente).
## Agradecimientos
Construido para [Hyprland](https://hyprland.org/), [PipeWire](https://pipewire.org/),
[Tailscale](https://tailscale.com/), [aiortc](https://github.com/aiortc/aiortc) y los
[dotfiles de end-4](https://github.com/end-4/dots-hyprland).