Network-controlled RGB LED panels using ESP32-C3 microcontrollers. Supports multiple panels in a grid configuration with 5 brightness levels per color channel (125 colors).
- Resolution: 64x16 pixels per panel
- Colors: 5 levels per channel (125 colors total)
- Driver ICs: MBI5034 (16-bit shift registers)
- Scan: 1/4 scan multiplexing
- Power: 5V, 2A+ per panel
Reference: https://wiki.london.hackspace.org.uk/view/LED_tiles_V2
| GPIO | Function |
|---|---|
| 8 | Data 1 (top rows) |
| 1 | Data 2 (bottom rows) |
| 4 | Clock |
| 7 | Latch |
| 2 | Output Enable |
| 3 | Address A0 |
| 6 | Address A1 |
- Use 5V 2A+ power supply per panel
- Connect ground between PSU, panel, and ESP32
- Do not power the panel through USB
Edit src/main.cpp:
const char WIFI_SSID[] = "YourNetwork";
const char WIFI_PASS[] = "YourPassword";Requires PlatformIO:
pio run -t uploadOn boot, each panel displays its IP address. After 10 seconds idle, it shows the IP again.
cd python
pip install pillowCreate displays.json to define your panel layout:
{
"brightness": 128,
"displays": [
{"ip": "192.168.1.100", "x": 0, "y": 0},
{"ip": "192.168.1.101", "x": 64, "y": 0},
{"ip": "192.168.1.102", "x": 0, "y": 16},
{"ip": "192.168.1.103", "x": 64, "y": 16}
]
}This creates a 2x2 grid (128x32 pixels total).
from ledsign import LEDSign
sign = LEDSign("192.168.1.100")
sign.set_pixel(0, 0, (4, 0, 0)) # Red at max brightness
sign.set_pixel(1, 0, (2, 2, 0)) # Yellow at 50%
sign.send()
sign.close()from ledsign import LEDSignArray
panels = [
("192.168.1.100", 0, 0),
("192.168.1.101", 64, 0),
]
sign = LEDSignArray(panels)
sign.set_pixel(65, 5, (0, 4, 0)) # Green on second panel
sign.send()
sign.close()Values are 0-4 per channel:
from ledsign import RED, GREEN, BLUE, WHITE, Color
sign.set_pixel(0, 0, RED) # Predefined color
sign.set_pixel(1, 0, (4, 2, 0)) # Orange as tuple
sign.set_pixel(2, 0, Color(1, 1, 1)) # Dim grayfrom PIL import Image
img = Image.open("image.png")
sign.load_image(img, dither=True)
sign.send()sign.set_brightness(128) # 0-255, affects all pixelsFor synchronized updates across panels:
sign = LEDSignArray(panels)
sign.set_sync_mode(True)
while True:
# Update buffer...
sign.send_synced() # Sends to all, then broadcasts syncpython demos.py matrix # Matrix rain
python demos.py fire # Animated fire
python demos.py plasma # Plasma waves
python demos.py starfield # Flying stars
python demos.py spectrum # Audio spectrum (fake)
python demos.py pong # Auto-playing pong
python demos.py gradient # Color test
python demos.py life # Game of Life
python demos.py wave # Sine waves
python demos.py clock # Digital clock
python demos.py scroller # Scrolling textOptions:
python demos.py matrix --ip=192.168.1.100 # Single panel
python demos.py matrix --config=my_config.json # Custom configpython gifplayer.py animation.gif
python gifplayer.py animation.gif --loop=3 # Play 3 times
python gifplayer.py animation.gif --speed=2.0 # Double speed
python gifplayer.py animation.gif --scale=fill # fill/fit/stretchpython plasma.py
python plasma.py --duration=30
python plasma.py --ip=192.168.1.100python snake.pyControls: Arrow keys or WASD
python fpstest.py
python fpstest.py --duration=10UDP port 5000.
| Offset | Size | Description |
|---|---|---|
| 0-1 | 2 | Magic "L5" |
| 2-3 | 2 | Frame number (uint16 LE) |
| 4-1027 | 1024 | Pixel data |
Pixel encoding: r*25 + g*5 + b where r,g,b are 0-4.
| Offset | Size | Description |
|---|---|---|
| 0-1 | 2 | Magic "LB" |
| 2 | 1 | Brightness 0-255 |
| 3 | 1 | Reserved |
| Offset | Size | Description |
|---|---|---|
| 0-1 | 2 | Magic "LM" |
| 2 | 1 | 0=immediate, 1=sync |
| 3 | 1 | Reserved |
LY\x00\x00 - Display buffered frame. Use broadcast (x.x.x.255) to sync all panels.
The panel uses 1/4 scan with time-division multiplexing for brightness levels:
- Timer ISR cycles through 4 banks × 4 brightness levels
- For each level, pixels with brightness >= threshold are lit
- Variable timing per level creates perceived brightness differences
Each refresh cycle shifts 384 bits per data line:
- 8 groups of 48 bits
- Order: Blue A, Blue B, Green A, Green B, Red A, Red B
Row mapping per bank:
| Bank | D1 Rows | D2 Rows |
|---|---|---|
| 0 | 0, 4 | 8, 12 |
| 1 | 1, 5 | 9, 13 |
| 2 | 2, 6 | 10, 14 |
| 3 | 3, 7 | 11, 15 |
No display: Check WiFi credentials, verify IP via serial monitor
Flickering: Reduce brightness, check power supply
Laggy: Run fpstest.py - expect 200+ fps network, 50+ fps with rendering
Out of sync: Use set_sync_mode(True) and send_synced()
MIT