Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1dd4560
feat: client side handling of new "Map" mode
le0m Sep 14, 2025
2353384
refactor: replace using an array of RGB bytes with a proper JPEG deco…
le0m Sep 14, 2025
22256d5
fix: merging new config config
le0m Sep 14, 2025
21ec0b7
feat: backend side handling of new "Map" mode
le0m Sep 14, 2025
97192e6
fix: changing point and route colors
le0m Sep 16, 2025
85070fc
feat: some responsive css
le0m Sep 20, 2025
79dc260
fix: concurrency problem when updating map configuration
le0m Sep 24, 2025
c0dcc6f
chore: increase app partition size
le0m Sep 24, 2025
02279ad
feat: implemented fetching of position from remote API
le0m Sep 26, 2025
c9d9611
fix: fail pre FS build if web app build fails
le0m Sep 26, 2025
91f70ed
fix: auth header and some ui stuff
le0m Sep 26, 2025
4addbdd
chore: shrink configs after copy
le0m Oct 31, 2025
723f080
refactor: open/close draw modes when changing mode, to hopefully redu…
le0m Nov 1, 2025
f6f285f
refactor: better handling of mode change, fixed a bunch of problems
le0m Nov 1, 2025
f341a39
fix: mode changing randomly
le0m Nov 1, 2025
07fe3e3
chore: update README
le0m Nov 1, 2025
fe2ae5e
refactor: stop using String
le0m Nov 2, 2025
a4fbea7
chore: minor ArduinoJSON changes
le0m Nov 2, 2025
9b466cc
chore: run prettier on save
le0m Nov 2, 2025
d93df43
refactor: state component for UI requests, upload progress bars
le0m Nov 2, 2025
6e2f967
fix: wifi IP string for QR code
le0m Nov 2, 2025
61d06f9
refactor: move files around
le0m Nov 2, 2025
bd29b83
chore: remove unused property
le0m Nov 2, 2025
098618b
fix: increase QR code version
le0m Nov 3, 2025
2170e35
refactor: simplify media
le0m Nov 7, 2025
37dda53
fix: use SSL client to fetch position from API
le0m Nov 8, 2025
5d41d9b
feat: print FS tree
le0m Nov 15, 2025
85dc8c5
chore: generic canvasToImage function
le0m Nov 15, 2025
00619ef
feat: support all image formats by converting to JPEG, as a "temporar…
le0m Nov 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,17 @@
"utility": "cpp",
"fstream": "cpp"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
Expand Down
126 changes: 61 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
This is a personal project to control a LED matrix from an ESP32 microcontroller.

<table>
<tr>
<td>
<p>Font</p>
<img src="https://github.com/user-attachments/assets/127f204d-a744-4bc3-8daa-86bc05b2e7ae" alt="Front" width="300">
</td>
<td>
<p>Back</p>
<img src="https://github.com/user-attachments/assets/ad2dc76f-5795-4f9d-8d62-73eaa76a2642" alt="Back" width="300">
</td>
<td>
<p>Inside overview</p>
<img src="https://github.com/user-attachments/assets/89290c72-be9d-48ae-81e2-061d67721fd4" alt="Inside overview" width="300">
</td>
</tr>
<tr>
<td>
<p>GY-521 glued to the inner border of the panel frame</p>
<img src="https://github.com/user-attachments/assets/09560be5-7bf0-49b4-9062-4507ca82acf4" alt="GY-521 glued to the inner border of the panel frame" width="300">
</td>
<td>
<p>ESP32 wires</p>
<img src="https://github.com/user-attachments/assets/b1536666-e6f3-48b7-b0cc-3fc9d728e89b" alt="ESP32 wires" width="300">
</td>
<td>
<p>Under the LED panel</p>
<img src="https://github.com/user-attachments/assets/fc978aba-f130-479f-9e3d-3d351aaf3a8e" alt="Under the LED panel" width="300">
</td>
</tr>
<tr>
<td>
<p>Image rendering</p>
<img src="https://github.com/user-attachments/assets/308a4614-1d05-4956-9a51-0c810895d79b" alt="Image rendering" width="300">
</td>
<td>
<p>GIF rendering</p>
<img src="https://github.com/user-attachments/assets/726eb4c8-b945-4988-a89a-f609bb3e5281" alt="GIF rendering" width="300">
</td>
<td>
<p>Rotation</p>
<img src="https://github.com/user-attachments/assets/76f52250-7bd9-4076-abf3-0fe4acd02f26" alt="Rotation" width="300">
</td>
</tr>
</table>

Some LEDs have been harmed in the development process. Our thoughts go to all the dead pixels out there~

# Components

My LED panel is 19,2x19,2cm, so I'm using a 25x25x3cm frame with a 3D-printed passepartout. I've screwed 4 screws through the frame's backpanel into the LED panel's screw holes, with spacers to keep the panel away from the backpanel and pressed against the glass frame.
Expand All @@ -8,7 +55,9 @@ The gyroscope is glued inside with the chip facing upwards (on the inner border

For powering it I'm using a random USB charger (5V 3A output): I've cut the mini-USB head and wired the esp32 board and LED panel (plus the gyroscope's ground because the esp32's were full). The panel actually uses up to 4-5A (depending on "brand", size and brightness) and the esp32 uses 0.5-1A, so I'm looking for a small power supply that can accomodate that, but it's currently working so ¯\\_(ツ)_/¯

See [gallery](#gallery) for photos.
Wiring/PIN layout

<img src="https://github.com/user-attachments/assets/639bfff5-33df-4ccd-8ace-b3b7bd0f3105" alt="Wiring/PIN layout" width="800">

## Hardware

Expand All @@ -24,7 +73,7 @@ See [gallery](#gallery) for photos.
- [FastLED](https://github.com/FastLED/FastLED), for color conversion functions and other utilities
- the ESP32Async project and their [AsyncTCP](https://github.com/ESP32Async/AsyncTCP)/[ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) libraries
- [ArduinoJSON](https://github.com/bblanchon/ArduinoJson), for JSON de/serialization
- [AnimatedGIF](https://github.com/bitbank2/AnimatedGIF), for decoding and drawing GIFs
- [AnimatedGIF](https://github.com/bitbank2/AnimatedGIF) and [JPEGDEC](https://github.com/bitbank2/JPEGDEC), for decoding GIFs and JPEGs
- project Nayuki's [qr-code-generator](https://github.com/nayuki/QR-Code-generator), for generating QR codes (included in `lib/` directory)
- [i2cdevlib](https://github.com/jrowberg/i2cdevlib), for reading the MPU6050 sensor (included in `lib/` directory)
- [NES.css](https://github.com/nostalgic-css/NES.css), for the NES-styled theme
Expand All @@ -40,85 +89,32 @@ This is why the `pathExists()` method exists.

# Usage

On first boot the esp32 creates a WiFi network with name `esp32-led-matrix` and password `daftpunk` (hardcoded in `src/wifi.h`). Connect to it, turn the frame upside down and scan the QR code to visit the web UI, where the WiFi name and password can be configured (stored in plain JSON in the flash memory). The esp32 will then connect to the provided WiFi and show a different QR code to visit the web UI on that network.
On first boot the esp32 creates a WiFi network with name `esp32-led-matrix` and password `daftpunk` (hardcoded in `src/wifi.h`). Connect to it, turn the frame upside down and scan the QR code to visit the web UI, where the WiFi SSID and password can be configured (stored in plain JSON in the flash memory). The esp32 will then connect to the provided WiFi and show a different QR code to visit the web UI on that network.

## Display modes

The LED panel has 4 display modes, depending on the frame orientation:
- ↑ shows image or GIF uploaded through the web UI
- → shows a [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway's_Game_of_Life) game
- ↓ shows the QR code
- ← shows nothing, TBD what to do
- ← shows a position on a map

## Web UI

- WiFi settings
- brightness (0-255)
- upload image or GIF, with preview of the scaled-down version; this file will be stored in the flash memory and loaded on boot, so the size is limited depending on the model of esp32 used (mine has 1.4MB and I'm reserving 0.4MB for the project's static data)
- upload image or GIF, with preview of the scaled-down version; this file will be stored in the flash memory and loaded on boot, so the size is limited depending on the model of esp32 used (mine has 1.4MB and I'm reserving some for the project's static data)
- map settings (upload map image, set position, use remote API)
- upload firmware updates (OTA)
- application logs (by default the logs are sent only if the web UI is loaded by at least 1 user, use the compile variable `LOG_QUEUE` to enable a queue of up to 50 messages that will be delivered once a user connects)

# Gallery

Wiring/PIN layout

<img src="https://github.com/user-attachments/assets/639bfff5-33df-4ccd-8ace-b3b7bd0f3105" alt="Wiring/PIN layout" width="800">

Some LEDs have been harmed in the development process. Our thoughts go to all the dead pixels out there~

<table>
<tr>
<td>
<p>Font</p>
<img src="https://github.com/user-attachments/assets/127f204d-a744-4bc3-8daa-86bc05b2e7ae" alt="Front" width="300">
</td>
<td>
<p>Back</p>
<img src="https://github.com/user-attachments/assets/ad2dc76f-5795-4f9d-8d62-73eaa76a2642" alt="Back" width="300">
</td>
</tr>
<tr>
<td>
<p>Inside overview</p>
<img src="https://github.com/user-attachments/assets/89290c72-be9d-48ae-81e2-061d67721fd4" alt="Inside overview" width="300">
</td>
<td>
<p>GY-521 glued to the inner border of the panel frame</p>
<img src="https://github.com/user-attachments/assets/09560be5-7bf0-49b4-9062-4507ca82acf4" alt="GY-521 glued to the inner border of the panel frame" width="300">
</td>
</tr>
<tr>
<td>
<p>ESP32 wires</p>
<img src="https://github.com/user-attachments/assets/b1536666-e6f3-48b7-b0cc-3fc9d728e89b" alt="ESP32 wires" width="300">
</td>
<td>
<p>Under the LED panel</p>
<img src="https://github.com/user-attachments/assets/fc978aba-f130-479f-9e3d-3d351aaf3a8e" alt="Under the LED panel" width="300">
</td>
</tr>
<tr>
<td>
<p>Image rendering</p>
<img src="https://github.com/user-attachments/assets/308a4614-1d05-4956-9a51-0c810895d79b" alt="Image rendering" width="300">
</td>
<td>
<p>GIF rendering</p>
<img src="https://github.com/user-attachments/assets/726eb4c8-b945-4988-a89a-f609bb3e5281" alt="GIF rendering" width="300">
</td>
</tr>
<tr>
<td>
<p>Rotation</p>
<img src="https://github.com/user-attachments/assets/76f52250-7bd9-4076-abf3-0fe4acd02f26" alt="Rotation" width="300">
</td>
</tr>
</table>
- resource usage and system info

# TODOs

- add FPS and other Life config
- send current GIF
- modeselector: prevent random wrong reads, use multiple reads in the span of some seconds
- a button to delete stored GIF and/or map image to free up space
- music visualizer (needs mic)
- setting for map remote API pull time (15m, 30m, 1h)
- map:
- zoom (need scaling on esp32)
- other position centering methods (center on point, use a margin to move the view before the position reaches the panel border, ...)
- store previous positions and show route
2 changes: 1 addition & 1 deletion data/config.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"wifi":{"ssid":"","password":""},"panel":{"brightness": 63},"log":{"level":2}}
{"wifi":{"ssid":"","password":""},"panel":{"brightness":63},"log":{"level":2},"map":{"latitude":"","longitude":"","url":"","method":"GET","regex":"","body":"","headers":"","pointSize":2,"pointColor":63488,"trackColor":31,"positions":[]}}
6 changes: 3 additions & 3 deletions partitions.csv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
spiffs, data, spiffs, 0x290000,0x160000,
app0, app, ota_0, 0x10000, 0x170000,
app1, app, ota_1, 0x180000,0x170000,
spiffs, data, spiffs, 0x2F0000,0x100000,
coredump, data, coredump,0x3F0000,0x10000,
4 changes: 4 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ lib_deps =
https://github.com/bblanchon/ArduinoJson#v7.3.0
; Play GIFs
https://github.com/bitbank2/AnimatedGIF.git#2.2.0
; Decode JPEGs
https://github.com/bitbank2/JPEGDEC.git#1.8.3
build_flags =
; esp32-idf library debug level
; -DCORE_DEBUG_LEVEL=3
; WiFi client debug level
; -ARDUHAL_LOG_LEVEL=4
; build with slimmed version of Adafruit GFX + FastLED
; -DUSE_GFX_LITE=1
; build without AdafruitGFX; no drawing, text or fonts!
Expand Down
3 changes: 2 additions & 1 deletion pre_buildfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def pre_gzip_static(source, target, env):
static_build_path = os.path.join(env.get('PROJECT_DIR'), 'web/dist')
www_dir_path = os.path.join(env.get('PROJECT_DIR'), 'data', 'www')
print('Building JS app in "web" to "web/dist"')
env.Execute("pnpm -C web release")
if env.Execute("pnpm -C web release") > 0:
raise ValueError('JS app build failed')
print('Compressing files from "web/dist" to "data/www" directory')
if os.path.exists(www_dir_path):
shutil.rmtree(www_dir_path)
Expand Down
5 changes: 5 additions & 0 deletions src/decoders/decoder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "decoder.h"

bool Decoder::isOpen() {
return isOpen_;
}
24 changes: 24 additions & 0 deletions src/decoders/decoder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#ifndef _DECODER_H
#define _DECODER_H

#include "../filesystem.h"
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
#include <ESPAsyncWebServer.h>

class Decoder {
protected:
bool isOpen_ = false;

public:
Decoder() {};
virtual ~Decoder() {};
virtual bool isOpen();
virtual bool deleteFile() = 0;
virtual bool writeFile(uint8_t*, size_t) = 0;
virtual bool sendFile(AsyncWebServerRequest*) = 0;
virtual bool open() = 0;
virtual void close() = 0;
virtual void renderFrame(MatrixPanel_I2S_DMA*) = 0;
};

#endif
Loading