Per què un protocol binari (i no JSON)
L'opció fàcil era JSON. També era la que feia impossible el target de 30 FPS. Un protocol binari de sis bytes d'overhead resol el coll d'ampolla.
L’opció més fàcil era enviar JSON. Era ràpid d’escriure, debuggable amb
tail -f, llegible per humans. També era la que feia fisicament impossible arribar a 30 FPS.
El context
La primera versió del host enviava hex ASCII: una línia per
LED amb el format 0xRRGGBB\n. Vuit caràcters + salt de
línia = 9 bytes per LED. Per a 299 LEDs: 2 691 bytes
per frame (afegint el \n final).
A 230 400 baud reals — comptant 10 bits per byte un cop sumat el bit de start i el d’stop —, el canal entrega uns 23 040 bytes per segon. Un frame, per anar pel cable, vol ~117 mil·lisegons. Sostre teòric: menys de 9 FPS, abans de comptar res del host, res de l’Arduino, res del CRC, res de cap recuperació d’errors.
El PRD demana 30 FPS sostingut a 300+ LEDs. La diferència entre 9 i 30 no és optimitzable: el protocol no pot.
La decisió
Frame binari minimalista, sense ambició:
Offset Mida Camp
0 1 SOF = 0xA5
1 1 VERSION = 0x01
2 1 TYPE (DATA=0x01, INIT=0x02, PING=0x03, ACK=0x04)
3 2 LEN (big-endian, uint16)
5 LEN PAYLOAD
5+LEN 1 CRC8 (cobreix VERSION..PAYLOAD-1)
Sis bytes d’overhead total per frame, independent del contingut. Quatre tipus de frame:
DATA: N × 3 bytes RGB. El pa de cada dia.INIT: 4 bytes — pin (uint16) + npixels (uint16). El host configura la tira en arrencar.PING: payload buit. Health-check.ACK: 1 byte de status. Resposta del firmware.
El mateix frame de 299 LEDs ara són 5 + 897 + 1 = 903 bytes. A 500 000 baud — el nou setpoint —, ~50 000 bytes/segon. Sostre teòric: ~55 FPS. Marge real per al target de 30 FPS amb gestió d’errors, retransmissions i backpressure inclosa.
Per què aquestes decisions concretes
Per què SOF = 0xA5. Un patró de bits 10100101 —
alterna prou perquè qualsevol byte stuck-low o stuck-high
es noti, i és un valor poc probable d’aparèixer
accidentalment a l’inici d’un payload RGB real.
Per què LEN big-endian. Cap raó d’eficiència; només
convenció de wire (“network byte order”). Un firmware en C
ho desempaqueta amb dues línies; el host Python igual.
Per què CRC8 i no CRC16 o cap. A 30 FPS i bytes
curts, una probabilitat d’1 / 256 de no detectar un error
és tolerable. El CRC16 dobla l’overhead per frame i no
detecta significativament millor a aquesta mida. Cap CRC
deixaria passar errors silenciosos — i amb la WS281x això
es tradueix en un LED random al mig de la tira, no en un
crash. Veure errors esporàdics és pitjor que detectar-los
i poder retransmetre.
Per què generadors a la decode. El codec del host
exposa decode(stream) -> Iterator[Frame]. Llegeix bytes
fins que troba un SOF ancorable; si en troba un de
qualsevol abans, el descarta silenciosament (resync).
És el mateix comportament que té el firmware. Si el cable
té soroll o queden bytes orfes entre frames concatenats, el
codec recupera sol sense pràcticament retransmissió.
Errors tipats, no booleans. La jerarquia
DecodeErrorté quatre subclasses:VersionMismatch,CRCMismatch,LengthError,UnknownFrameType. Importa perquè la resposta del host és diferent per a cadascuna. UnCRCMismatches retransmet; unVersionMismatchaborta i renegocia (el firmware corre una build antiga, per exemple); unLengthErrorno es retransmet — el frame estava mal format. TornarFalseoNoneperdria aquesta distinció i amagaria el bug del host darrere d’una retransmissió que mai funcionaria.
El que ve
Tenim un protocol que cap pel cable. Ara cal un host que
sàpiga generar frames a 30 FPS, fer-ho mentre la UI web
demana coses, i no congelar-se quan el camí ric (tick → encode → write → ack) es desincronitza una mil·lèsima.
Pròxim post: l’arquitectura Python.