Skip to content

← Volver al listado

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.

3 min de lectura

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.

Esquema de la UI web local: cabecera con estado de conexión, canvas de preview, sliders de tuning (FPS, gamma, kelvin, brightness) y franja de métricas.

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 virtual el 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 patrones calib-*.png en 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/framesbytes 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/eventsJSON 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:8080 pero 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.