La UI web local: FastAPI dins l'app
Un comandament físic seria òptim. Construir-lo, no. Vaig posar la UI al navegador i la vaig fer servir des de qualsevol dispositiu de la xarxa.
Un comandament físic amb potenciòmetres seria òptim. Construir-lo, no. Vaig posar la UI al navegador i la vaig fer accessible des de qualsevol dispositiu de la xarxa local. Una decisió pragmàtica, no una declaració.
El context
Lumware corre al portàtil o a una Raspberry. Però vull poder ajustar la brillantor des del mòbil mentre toco la tira físicament a tres metres de la pantalla. Una app nativa per cada plataforma és absurda per a un projecte personal. Un servidor web local resol tot d’un cop:
- Cap instal·lació al dispositiu de control — només un navegador.
- Mateixa URL des de mòbil, tauleta o portàtil.
- Zero permisos d’app store, zero builds creuades.
El cost és afegir un servidor al procés del host. Però el host ja és un procés persistent — un servidor més no canvia el model.
Preview = strip. Amb
--transport virtualel host arrenca sense Arduino: el frame final acaba a un ring buffer en memòria i la UI rep exactament els mateixos bytes que rebria la tira. La primera vegada que arrenca, l’app sembra els patronscalib-*.pngal directori de recursos per poder prémer Play immediatament.
La decisió
FastAPI per HTTP + WebSocket. React + Vite + Tailwind per al frontend, construït a estàtic i servit per la mateixa FastAPI. Tot empaquetat amb PyInstaller en un sol binari.
Dos canals WebSocket
/ws/frames — bytes binaris crus. Cada missatge és un
frame: N × 3 bytes RGB, sense JSON, sense capçaleres,
sense codec. Exactament el mateix byte sequence que el
codec envia al transport, però abans de l’encoding del
frame binari (sense SOF, sense CRC). Per què: així el
preview a la UI és per disseny el mateix que la tira
veu. No hi pot haver “preview no coincideix amb tira”
perquè és literalment la mateixa font.
/ws/events — JSON dels events del bus. La UI rep
PLAYBACK_METRICS, TRANSPORT_*, SETTINGS_CHANGED, etc.
Dibuixa gràfics, actualitza l’estat de connexió, mostra
logs. Si vols afegir una nova vista, subscriu-te a un
event que ja existeix.
REST per al que no canvia 30 cops per segon
GET /api/health— viu?GET /api/ports— autodetecció de ports sèrie.POST /api/connection/connect—{port, baud}.POST /api/playback/{play,pause,stop}.GET /api/resources— llistat d’imatges/vídeos.PATCH /api/settings— patch parcial de Settings.
Cada handler agafa les seves dependències via Depends()
de FastAPI. Els tests usen TestClient i construeixen les
deps manualment — el factory create_app(...) és el seam.
LAN-only per disseny. No hi ha auth. CORS és wide-open. Això sembla una vulnerabilitat fins que recordes l’escenari real: un sol usuari, xarxa privada, dispositiu físic connectat al host per USB. Si algú té accés a la teva LAN i et vol fotre, té problemes més grossos que una API que canvia el gamma de la teva tira. Afegir login complica el codi, complica la UX (re-autenticar al mòbil cada cop?), i no aporta seguretat real al model d’amenaça. El servidor escolta a
0.0.0.0:8080però el README és explícit: no exposeu a internet.
El frontend és React per pragmatisme
No per estètica. Vite + React + Tailwind és el que sé
muntar més ràpid amb tests (vitest), tipat (tsconfig.app)
i build determinístic. La story 07-07 empaqueta el
dist/ de Vite dins del bundle PyInstaller — així el
binari final inclou frontend i backend en un sol arxiu.
Quan no hi ha build encara, FastAPI serveix un
placeholder HTML amb la versió i una nota
“compileu el frontend amb npm run build”.
El que ve
Tot el sistema és damunt la taula. Falta el moment de la veritat: els números. Quants FPS aconseguim realment, quina latència té el cable, on és el coll d’ampolla. Últim post de la sèrie: framerate i bottlenecks.