Skip to content

← Volver al listado

Por qué un protocolo binario (y no JSON)

La opción fácil era JSON. También era la que hacía imposible el target de 30 FPS. Un protocolo binario de seis bytes de overhead resuelve el cuello de botella.

4 min de lectura

La opción más fácil era enviar JSON. Era rápido de escribir, debuggable con tail -f, legible por humanos. También era la que hacía físicamente imposible llegar a 30 FPS.

Estructura del frame binario: SOF (0xA5), VERSION, TYPE, LEN big-endian, PAYLOAD y CRC8.

El contexto

La primera versión del host enviaba hex ASCII: una línea por LED con formato 0xRRGGBB\n. Ocho caracteres + salto de línea = 9 bytes por LED. Para 299 LEDs: 2 691 bytes por frame (sumando el \n final).

A 230 400 baudios reales — contando 10 bits por byte una vez sumados el bit de start y el de stop —, el canal entrega unos 23 040 bytes por segundo. Un frame, para ir por el cable, quiere ~117 milisegundos. Techo teórico: menos de 9 FPS, antes de contar nada del host, nada del Arduino, nada del CRC, nada de recuperación de errores.

El PRD pide 30 FPS sostenidos a 300+ LEDs. La diferencia entre 9 y 30 no es optimizable: el protocolo no puede.

La decisión

Frame binario minimalista, sin ambición:

Offset  Tamaño  Campo
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      (cubre VERSION..PAYLOAD-1)

Seis bytes de overhead total por frame, independiente del contenido. Cuatro tipos de frame:

  • DATA: N × 3 bytes RGB. El pan de cada día.
  • INIT: 4 bytes — pin (uint16) + npixels (uint16). El host configura la tira al arrancar.
  • PING: payload vacío. Health-check.
  • ACK: 1 byte de status. Respuesta del firmware.

El mismo frame de 299 LEDs ahora son 5 + 897 + 1 = 903 bytes. A 500 000 baudios — el nuevo setpoint —, ~50 000 bytes/segundo. Techo teórico: ~55 FPS. Margen real para el target de 30 FPS con gestión de errores, retransmisiones y backpressure incluidos.

Comparativa antes/después: bytes por frame y FPS techo — ASCII hex (2691 B, ~9 FPS) vs binario (903 B, ~55 FPS).

Por qué estas decisiones concretas

Por qué SOF = 0xA5. Un patrón de bits 10100101 — alterna lo bastante para que cualquier byte stuck-low o stuck-high se note, y es un valor poco probable de aparecer accidentalmente al inicio de un payload RGB real.

Por qué LEN big-endian. Ninguna razón de eficiencia; solo convención de wire (“network byte order”). Un firmware en C lo desempaqueta con dos líneas; el host Python, igual.

Por qué CRC8 y no CRC16 o ninguno. A 30 FPS y bytes cortos, una probabilidad de 1 / 256 de no detectar un error es tolerable. El CRC16 dobla el overhead por frame y no detecta significativamente mejor a este tamaño. Ningún CRC dejaría pasar errores silenciosos — y con WS281x eso se traduce en un LED random a mitad de la tira, no en un crash. Ver errores esporádicos es peor que detectarlos y poder retransmitir.

Por qué generadores en el decode. El codec del host expone decode(stream) -> Iterator[Frame]. Lee bytes hasta que encuentra un SOF anclable; si encuentra cualquier otro byte antes, lo descarta silenciosamente (resync). Es el mismo comportamiento que tiene el firmware. Si el cable tiene ruido o quedan bytes huérfanos entre frames concatenados, el codec se recupera solo sin prácticamente retransmisión.

Errores tipados, no booleanos. La jerarquía DecodeError tiene cuatro subclases: VersionMismatch, CRCMismatch, LengthError, UnknownFrameType. Importa porque la respuesta del host es distinta para cada una. Un CRCMismatch se retransmite; un VersionMismatch aborta y renegocia (el firmware corre una build antigua, por ejemplo); un LengthError no se retransmite — el frame estaba mal formado. Devolver False o None perdería esta distinción y escondería el bug del host detrás de una retransmisión que nunca funcionaría.

Lo que viene

Tenemos un protocolo que cabe por el cable. Ahora hace falta un host que sepa generar frames a 30 FPS, hacerlo mientras la UI web pide cosas, y no congelarse cuando el camino feliz (tick → encode → write → ack) se desincroniza una milésima. Próximo post: la arquitectura Python.