La UI web local: FastAPI dentro de la app
Un mando físico sería óptimo. Construirlo, no. Puse la UI en el navegador y la hice accesible desde cualquier dispositivo de la red.
Un mando físico con potenciómetros sería óptimo. Construirlo, no. Puse la UI en el navegador y la hice accesible desde cualquier dispositivo de la red local. Una decisión pragmática, no una declaración.
El contexto
Lumware corre en el portátil o en una Raspberry. Pero quiero poder ajustar el brillo desde el móvil mientras toco la tira físicamente a tres metros de la pantalla. Una app nativa por cada plataforma es absurda para un proyecto personal. Un servidor web local resuelve todo de golpe:
- Cero instalación en el dispositivo de control — solo un navegador.
- Misma URL desde móvil, tablet o portátil.
- Cero permisos de app store, cero builds cruzadas.
El coste es añadir un servidor al proceso del host. Pero el host ya es un proceso persistente — un servidor más no cambia el modelo.
Preview = strip. Con
--transport virtualel host arranca sin Arduino: el frame final acaba en un ring buffer en memoria y la UI recibe exactamente los mismos bytes que recibiría la tira. La primera vez que arranca, la app siembra los patronescalib-*.pngen el directorio de recursos para poder pulsar Play inmediatamente.
La decisión
FastAPI para HTTP + WebSocket. React + Vite + Tailwind para el frontend, construido a estático y servido por la propia FastAPI. Todo empaquetado con PyInstaller en un solo binario.
Dos canales WebSocket
/ws/frames — bytes binarios crudos. Cada mensaje es
un frame: N × 3 bytes RGB, sin JSON, sin cabeceras, sin
codec. Exactamente la misma byte sequence que el codec
envía al transport, pero antes del encoding del frame
binario (sin SOF, sin CRC). Por qué: así el preview en la
UI es por diseño el mismo que ve la tira. No puede
haber “preview no coincide con tira” porque es literalmente
la misma fuente.
/ws/events — JSON de los eventos del bus. La UI
recibe PLAYBACK_METRICS, TRANSPORT_*,
SETTINGS_CHANGED, etc. Dibuja gráficos, actualiza el
estado de conexión, muestra logs. Si quieres añadir una
nueva vista, te suscribes a un evento que ya existe.
REST para lo que no cambia 30 veces por segundo
GET /api/health— ¿vivo?GET /api/ports— autodetección de puertos serie.POST /api/connection/connect—{port, baud}.POST /api/playback/{play,pause,stop}.GET /api/resources— listado de imágenes/vídeos.PATCH /api/settings— patch parcial de Settings.
Cada handler toma sus dependencias vía Depends() de
FastAPI. Los tests usan TestClient y construyen las deps
manualmente — el factory create_app(...) es el seam.
LAN-only por diseño. No hay auth. CORS es wide-open. Eso parece una vulnerabilidad hasta que recuerdas el escenario real: un solo usuario, red privada, dispositivo físico conectado al host por USB. Si alguien tiene acceso a tu LAN y quiere joderte, tiene problemas más grandes que una API que cambia el gamma de tu tira. Añadir login complica el código, complica la UX (re-autenticarse en el móvil cada vez?) y no aporta seguridad real al modelo de amenaza. El servidor escucha en
0.0.0.0:8080pero el README es explícito: no expongas a internet.
El frontend es React por pragmatismo
No por estética. Vite + React + Tailwind es lo que sé
montar más rápido con tests (vitest), tipado
(tsconfig.app) y build determinístico. La story 07-07
empaqueta el dist/ de Vite dentro del bundle PyInstaller
— así el binario final incluye frontend y backend en un
solo archivo. Cuando aún no hay build, FastAPI sirve un
placeholder HTML con la versión y una nota “compila el
frontend con npm run build”.
Lo que viene
Todo el sistema está sobre la mesa. Falta el momento de la verdad: los números. Cuántos FPS conseguimos realmente, qué latencia tiene el cable, dónde está el cuello de botella. Último post de la serie: framerate y bottlenecks.