Turn animated GIFs into playable animations for a NeoPixel-compatible LED matrix. The project combines a browser-based editor with MicroPython firmware that serves the editor, stores animation data on a microSD card, and plays it back on a microcontroller.
This is a hardware project. It has been developed around MicroPython, an ESP8266-style SPI configuration, a microSD card, and NeoPixel/WS2812 LEDs.
Watch the project in action: GIF to LED Matrix demo on YouTube.
This project was originally developed for the ESP32 LED Motor Driver board on Hackaday. Its MicroPython-based design can be adapted to other compatible boards with the appropriate pin, storage, and NeoPixel configuration.
- Loads an animated GIF from your computer or a URL.
- Crops and scales frames to the dimensions of an LED display.
- Lets you touch up individual pixels in a browser-based editor.
- Transfers animation data to the device over WebSocket.
- Stores animations on the SD card and plays
autoplayafter boot.
firmware/
├── boot.py # Wi-Fi setup and SD-card mount
├── led_server.py # Web server, upload protocol, and playback
├── web/ # Browser editor served by the device
├── src/ # Source for modules deployed as .mpy files
└── *.mpy # Precompiled MicroPython dependencies
- A board running MicroPython with NeoPixel support. The original target was the ESP32 LED Motor Driver board; the included SD wiring uses ESP8266-style pin assignments and should be adjusted for other hardware.
- A NeoPixel/WS2812-compatible display. Configure its data pin and dimensions in the web editor.
- A microSD card formatted for MicroPython/FAT storage.
- A microSD SPI breakout wired as defined in
boot.py:- SCK: GPIO 14
- MOSI: GPIO 13
- MISO: GPIO 12
- CS: GPIO 15
- A modern browser on the same Wi-Fi network as the device.
The browser editor is self-contained: its jQuery and jQuery UI dependencies are included in firmware/web/vendor, so the device does not need Internet access to serve the editor.
- Flash a compatible MicroPython build to the board.
- Copy the contents of
firmwareto the device filesystem. Keep thewebdirectory intact and deploy the included.mpyfiles alongsideboot.pyandled_server.py. - Edit
ssidandpasswordinfirmware/boot.pybefore uploading it. Do not commit real credentials. - Insert the SD card and reset the board. Its serial output prints the assigned IP address.
- Visit
http://<device-ip>/from a computer on the same network. - Load a GIF, define the crop and display properties, add the NeoPixel device, edit frames if needed, then upload.
Uploaded animations are stored on the SD card under /sd/<name>/ as cfg.dat and dat.dat. The current editor uploads to the autoplay slot, which the firmware attempts to play at startup.
Each animation lives in /sd/<name>/ and consists of a text configuration file and a raw binary data file. The upload protocol writes the configuration first, then appends the binary data in 512-byte WebSocket chunks.
cfg.dat is a UTF-8, newline-delimited file. The WebSocket upload header contains S and the animation name first; those two values are not written to cfg.dat.
| Line | Field | Meaning |
|---|---|---|
| 1 | frame_count |
Number of frames in the animation. Retained as metadata; the current player loops when it reaches the end of dat.dat. |
| 2 | width |
Animation width metadata. The current browser uploader writes 0; playback does not use this value to size a device. |
| 3 | height |
Animation height metadata. The current browser uploader writes 0; playback does not use this value to size a device. |
| 4 | delay |
Delay between frames in centiseconds (6 = 60 ms). |
| 5 | device_count |
Number of output devices. |
| 6 onward | device records | One three-line record per device: type, pin, then data_length in bytes. |
The current firmware implements device type 2, a NeoPixel-compatible output. Its data_length must be pixel_count × 3. Other type values are not supported by the current player.
For example, a three-frame animation for one 8×8 NeoPixel matrix on pin 2, at 60 ms per frame, has this configuration:
3
0
0
6
1
2
2
192
dat.dat is raw binary. Its payload is ordered by frame, then by device record order within that frame:
frame 0: device 0 bytes, device 1 bytes, ...
frame 1: device 0 bytes, device 1 bytes, ...
...
For a type-2 NeoPixel device, each pixel occupies three bytes in GRB order. The browser editor converts its canvas pixels from RGBA to GRB and removes alpha. For a well-formed finite animation, the expected file size is:
frame_count × sum(all device data_length values)
In the 8×8 example above, each frame is 192 bytes and a three-frame dat.dat is 576 bytes.
At boot, the firmware attempts to play /sd/autoplay/cfg.dat and /sd/autoplay/dat.dat. During playback, Player.render() reads one data_length payload for each configured device, assigns it to the device, then waits delay / 100 seconds. When a read reaches the end of dat.dat, the player seeks back to byte zero and continues, so the animation loops indefinitely.
When an editor connects, it sends R; the firmware stops active playback and replies READY. The editor then sends the configuration header and waits for OK before streaming binary chunks. Sending E ends the transfer, the firmware replies DONE, and playback starts after the WebSocket closes.
The Python files are checked for parse errors on every pull request and push. You can run the same check locally:
python -m compileall -q firmwareThis verifies Python syntax only; hardware behavior must be tested on a device with the intended LED matrix and SD card.
The deployable .mpy modules are generated from the snake_case sources in firmware/src. They target MicroPython v1.11 and must be rebuilt with the matching cross-compiler; .mpy bytecode is version-specific.
python -m pip install "mpy-cross==1.11"
python -m mpy_cross -o firmware/micro_web_server.mpy firmware/src/micro_web_server.py
python -m mpy_cross -o firmware/micro_web_socket.mpy firmware/src/micro_web_socket.py
python -m mpy_cross -o firmware/micro_web_template.mpy firmware/src/micro_web_template.py
python -m mpy_cross -o firmware/sd_card.mpy firmware/src/sd_card.pyThe led_server.py application is intentionally deployed as source because it is the user-facing entry point imported by boot.py.
- GIFs loaded from a URL must allow cross-origin browser requests; uploading a local GIF avoids that restriction.
- Large GIFs can exceed the available memory in the browser or on the board. Start with short, low-resolution animations.
- The web editor currently exposes NeoPixel output. The firmware's upload format is tailored to the included player.
This project includes or uses the following upstream open-source projects:
- MicroWebSrv — the MicroPython HTTP, WebSocket, and template server code.
- MicroPython-lib — the SPI SD-card driver.
- gifuct-js — GIF parsing and frame decompression.
- Spectrum — the color picker.
- jQuery and jQuery UI — browser UI dependencies.
See the vendored source headers and the linked upstream repositories for their license terms. This project itself is licensed under the GNU GPL v3.0.
Issues and pull requests are welcome. Please keep changes compatible with MicroPython's constrained runtime, avoid committing Wi-Fi credentials or generated firmware artifacts, and test both the browser editor and target hardware when changing the transfer or playback path.