feat(03-01): implement WLED protocol encoders and animation command packet
- encode_drgb/encode_drgbw: 2-byte header + RGB/RGBW body, truncate at MTU max - encode_dnrgb_packets: splits large frames into 489-LED chunks with big-endian start index - encode_animation_cmd: 16-byte 0xAC packet with speed (float32), color, flags, density - decode_animation_cmd: full inverse round-trip for all 7 animation types - All 27 tests pass
This commit is contained in:
71
lightsync/protocol/drgb.py
Normal file
71
lightsync/protocol/drgb.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""WLED-compatible UDP packet encoders — DRGB, DRGBW, DNRGB.
|
||||
|
||||
Source: https://kno.wled.ge/interfaces/udp-realtime/
|
||||
"""
|
||||
import struct
|
||||
|
||||
WLED_PORT = 21324
|
||||
PROTOCOL_DRGB = 2
|
||||
PROTOCOL_DRGBW = 3
|
||||
PROTOCOL_DNRGB = 4
|
||||
TIMEOUT_BYTE = 2 # seconds before device returns to normal mode
|
||||
|
||||
# Max pixels per packet (stays under 1472-byte UDP payload limit)
|
||||
# Standard Ethernet MTU = 1500; IP header = 20; UDP header = 8 → max payload = 1472
|
||||
DRGB_MAX_PX = 490 # (1472 - 2) // 3
|
||||
DRGBW_MAX_PX = 367 # (1472 - 2) // 4
|
||||
DNRGB_MAX_PX = 489 # (1472 - 4) // 3
|
||||
|
||||
|
||||
def encode_drgb(pixels: list[tuple[int, int, int]]) -> bytes:
|
||||
"""Encode up to 490 RGB pixels into a DRGB UDP packet.
|
||||
|
||||
Format: [0x02][timeout][R0][G0][B0]...[Rn][Gn][Bn]
|
||||
Truncates silently to DRGB_MAX_PX if more pixels are provided.
|
||||
"""
|
||||
header = bytes([PROTOCOL_DRGB, TIMEOUT_BYTE])
|
||||
body = bytearray()
|
||||
for r, g, b in pixels[:DRGB_MAX_PX]:
|
||||
body.extend([r & 0xFF, g & 0xFF, b & 0xFF])
|
||||
return header + bytes(body)
|
||||
|
||||
|
||||
def encode_drgbw(pixels: list[tuple[int, int, int, int]]) -> bytes:
|
||||
"""Encode up to 367 RGBW pixels into a DRGBW UDP packet.
|
||||
|
||||
Format: [0x03][timeout][R0][G0][B0][W0]...[Rn][Gn][Bn][Wn]
|
||||
Truncates silently to DRGBW_MAX_PX if more pixels are provided.
|
||||
"""
|
||||
header = bytes([PROTOCOL_DRGBW, TIMEOUT_BYTE])
|
||||
body = bytearray()
|
||||
for r, g, b, w in pixels[:DRGBW_MAX_PX]:
|
||||
body.extend([r & 0xFF, g & 0xFF, b & 0xFF, w & 0xFF])
|
||||
return header + bytes(body)
|
||||
|
||||
|
||||
def encode_dnrgb_packets(
|
||||
pixels: list[tuple[int, int, int]],
|
||||
start_index: int = 0,
|
||||
) -> list[bytes]:
|
||||
"""Split a large RGB pixel array into DNRGB UDP packets, each under MTU.
|
||||
|
||||
Format per packet: [0x04][timeout][start_hi][start_lo][R0][G0][B0]...[Rn][Gn][Bn]
|
||||
Start index is 16-bit big-endian per WLED spec.
|
||||
|
||||
Args:
|
||||
pixels: Full list of (R, G, B) pixel tuples.
|
||||
start_index: LED strip offset for the first pixel (default 0).
|
||||
|
||||
Returns:
|
||||
List of byte strings, one per chunk of up to DNRGB_MAX_PX pixels.
|
||||
"""
|
||||
packets = []
|
||||
for offset in range(0, len(pixels), DNRGB_MAX_PX):
|
||||
chunk = pixels[offset:offset + DNRGB_MAX_PX]
|
||||
idx = start_index + offset
|
||||
header = struct.pack(">BBH", PROTOCOL_DNRGB, TIMEOUT_BYTE, idx)
|
||||
body = bytearray()
|
||||
for r, g, b in chunk:
|
||||
body.extend([r & 0xFF, g & 0xFF, b & 0xFF])
|
||||
packets.append(header + bytes(body))
|
||||
return packets
|
||||
Reference in New Issue
Block a user