Turn your OBEGRÄNSAD LED Wall Lamp into a live drawing canvas
⚠ Disclaimer: Use this code and instructions at your own risk! Improper use may damage the device. Contribute: Have suggestions or improvements? Feel free to submit a PR or open an issue. 😊
- Features
- Quick Start
- Hardware Setup
- Software Setup
- OTA Updates
- HTTP API Reference
- DDP (Display Data Protocol)
- Home Assistant Integration
- Development
- Troubleshooting
Click to expand feature list
General Features:
- Persist your drawing
- Rotate image
- Live Drawing
- OTA Update
- WiFi Control
- Web GUI
- Load an image
- Switch plugin by pressing the button
- Schedule Plugins to switch after "n" seconds
Available Plugins:
- Draw
- Game of Life
- Breakout
- Snake
- Stars
- Lines
- Circle
- Clock
- Big Clock
- Weather
- Rain
- Animation (with Animation Creator in Web UI)
- Firework
- DDP (Display Data Protocol)
- Pong Clock
Area.mp4
Control the lamp using the built-in web GUI. Find the device IP address via:
- Serial monitor output
- Router admin panel
This software is designed for ESP32 Dev Boards but can work with other Arduino boards (WiFi, OTA, and web server features will need to be removed for non-ESP boards).
Supported Boards:
- ESP32 Dev Board (recommended)
- TTGO LoRa32 V2.1 (T3_V1.6.1)
- ESP8266 (with limitations: per-pixel brightness only works when storage and global brightness are disabled)
IKEA uses rivets instead of regular screws. To open:
- Insert a screwdriver between the rivets and back panel
- Carefully pry open with a second object
- Alternative: Drill out the rivets (cleaner but permanent)
Inside you'll find 4 identical plates with 64 LEDs each (in 4 fields). Focus on the lowest plate:
- 6 connectors at the bottom edge (connection points for your board)
- Original microcontroller at the top (must be removed)
Connect the pins as shown below. Remember to configure them in include/constants.h according to your board.
| LCD | ESP32 | TTGO LoRa32 | NodeMCUv2 | Lolin D32 (Pro) | Xiao ESP32S3 |
|---|---|---|---|---|---|
| GND | GND | GND | GND | GND | GND |
| VCC | 5V | 5V | VIN | USB | VUSB |
| EN (PIN_ENABLE) | GPIO26 | IO22 | GPIO16 D0 | GPIO26 | D4 (GPIO5) |
| IN (PIN_DATA) | GPIO27 | IO23 | GPIO13 D7 | GPIO27 | D10 (MOSI) |
| CLK (PIN_CLOCK) | GPIO14 | IO02 | GPIO14 D5 | GPIO14 | D8 (SCK) |
| CLA (PIN_LATCH) | GPIO12 | IO15 | GPIO0 D3 | GPIO12 | D5 (GPIO6) |
| BUTTON one end | GPIO16 | IO21 | GPIO2 D4 | GPIO25 | D3 (GPIO4) |
| BUTTON other end | GND | GND | GND | GND | GND |
You can use the original button wiring without adding external connections. See this issue by RBEGamer for details.
-
Prerequisites
- Install Visual Studio Code
- Install the PlatformIO IDE extension from VS Code Extensions Marketplace
-
Clone the Project
git clone git@github.com:ph1p/ikea-led-obegraensad.git cd ikea-led-obegraensad code .
PlatformIO will automatically load dependencies.
-
Connect ESP32
- Connect your ESP32 via USB
- Verify the COM port in the PlatformIO Devices tab
-
Configure the Project
- Run
PlatformIO: Clean(Recycle bin icon in bottom toolbar) - Edit
include/secrets.hwith your WiFi credentials (ESP8266 only; ESP32 can use WiFi Manager) - Configure variables in
include/constants.h
- Run
-
Build the Project
- Click the
PlatformIO Buildicon (bottom toolbar) - If libraries are missing, install them via the PlatformIO Libraries tab
- Repeat
CleanandBuilduntil successful
- Click the
-
Upload to ESP32
- Click
PlatformIO Upload(bottom toolbar)
- Click
ESP32 (WiFi Manager - Recommended):
This project uses tzapu's WiFiManager. After booting:
- Device attempts to connect to known access points
- If none available, creates network named
Ikea Display Setup WiFi - Connect to this network on any device
- Captive portal guides you through WiFi configuration
- Device reboots and connects to your network
*Network name can be changed via WIFI_MANAGER_SSID in include/constants.h.
ESP8266 (Manual Configuration):
For ESP8266, WiFi Manager is not available. Set WIFI_SSID and WIFI_PASSWORD in include/secrets.h.
Over-The-Air (OTA) updates allow you to upload new firmware wirelessly without a USB connection. This is powered by ElegantOTA.
Before using OTA, configure the following:
-
Set OTA Credentials in
include/secrets.h:#define OTA_USERNAME "admin" #define OTA_PASSWORD "your-password"
-
Configure Upload Settings in
platformio.ini(for theesp32devenvironment):extra_scripts = upload.py upload_protocol = custom custom_upload_url = http://192.168.178.50 # Your device IP custom_username = admin custom_password = your-password
Note: Replace 192.168.178.50 with your device's actual IP address.
- Navigate to
http://your-device-ip/updatein your browser - Login with your configured credentials (default:
admin/ikea-led-wall) - Select your firmware file (
.pio/build/esp32dev/firmware.bin) - Click "Update" and wait for completion
- Device will automatically reboot with new firmware
Upload directly from PlatformIO via the command line:
pio run -e esp32dev -t uploadOr use the PlatformIO Upload button in VS Code (bottom toolbar).
Requirements:
- Python packages:
requests_toolbeltandtqdm - Install if needed:
pip install requests_toolbelt tqdm
During OTA updates, the LED matrix provides visual feedback:
- "U" letter displayed: Update has started
- Serial output: Progress updates every second
- "R" letter displayed: Update completed (device will reboot)
Monitor the serial output for detailed progress:
OTA update started!
OTA Progress Current: 262144 bytes, Final: 1440655 bytes
OTA Progress Current: 524288 bytes, Final: 1440655 bytes
...
OTA update finished successfully!
Troubleshooting:
- Ensure device is connected to the same network
- Verify IP address in
platformio.inimatches device IP - Check credentials match in both
secrets.handplatformio.ini - For upload failures, try the web interface method first
Base URL: http://your-server/api
Get device info, current state, and available plugins
GET /api/infoExample:
curl http://your-server/api/infoResponse:
{
"rows": 16,
"cols": 16,
"status": "active",
"plugin": 3,
"rotation": 90,
"brightness": 255,
"scheduleActive": true,
"schedule": [
{ "pluginId": 2, "duration": 60 },
{ "pluginId": 4, "duration": 120 }
],
"plugins": [
{ "id": 1, "name": "Plugin One" },
{ "id": 2, "name": "Plugin Two" }
]
}Set active plugin by ID
PATCH /api/plugin?id={plugin_id}Example:
curl -X PATCH "http://your-server/api/plugin?id=7"Response:
{
"status": "success",
"message": "Plugin set successfully"
}Error Response:
{
"error": true,
"errormessage": "Could not set plugin to id 7"
}PATCH /api/brightness?value={0-255}Example:
curl -X PATCH "http://your-server/api/brightness?value=100"Response:
{
"status": "success",
"message": "Brightness set successfully"
}Retrieve current display data as a byte-array. Each byte represents pixel brightness (0-255). Global brightness is applied after these values.
GET /api/dataExample:
curl http://your-server/api/dataResponse:
[255, 255, 255, 0, 128, 255, 255, 0, ...]Display scrolling text and/or graphs on the LED matrix.
GET /api/messageParameters:
text(optional): Text message to displaygraph(optional): Comma-separated integers (0-15) representing a graphminy(optional): Graph lower bound (default: 0)maxy(optional): Graph upper bound (default: 15)repeat(optional): Repetition count (default: 1, use-1for infinite)id(optional): Unique identifier for the messagedelay(optional): Scroll delay in ms (default: 50)
Example:
curl "http://your-server/api/message?text=Hello&graph=8,5,2,1,0,0,1,4,7,10,13,14,15,15,14,11&repeat=3&id=1&delay=60"Response:
{
"status": "success",
"message": "Message received"
}GET /api/removemessage?id={message_id}Example:
curl "http://your-server/api/removemessage?id=1"Automatically switch between plugins on a schedule.
POST /api/scheduleExample:
curl -X POST http://your-server/api/schedule -d 'schedule=[{"pluginId":10,"duration":2},{"pluginId":8,"duration":5}]'GET /api/schedule/startGET /api/schedule/stopGET /api/schedule/clearGET /api/clearstorageExample:
curl http://your-server/api/clearstorageDDP enables real-time LED matrix control via UDP packets. External applications can send pixel data directly over the network.
Please install Poetry for an easy way to setup the Python environment.
To install all required dependencies and create a virtual environment run:
poetry installVS Code should detect the venv automatically and prompt you to activate it. Once that's done you can use the python command as usual. Alternatively use the poetry run command to make sure the python from the virtual environment is executed, e.g.:
poetry run python ddp.py clear-
Enable DDP Plugin
curl -X PATCH "http://your-server/api/plugin?id=17" -
Send Pixels
python3 ddp.py --ip 192.168.178.50 --fill 128
The included Python script (ddp.py) simplifies DDP packet creation. It offers a rudimentary command line interface to control display content.
Clear all pixels:
python3 ddp.py clear --ip 192.168.178.50Fill display with brightness value:
python3 ddp.py fill 128 --ip 192.168.178.50Set individual pixels (X, Y, brightness):
python3 ddp.py pixels --ip 192.168.178.50 -p 0 0 255 -p 15 15 128Subcommands:
clear: Clear all pixelsfill BRIGHTNESS: Fill with brightness (0-255)pixels -p X Y BRIGHTNESS: Set pixel (-p can be used multiple times)
Options (for all subcommands):
--ip: Display IP address (default: 192.168.178.50)--port: UDP port (default: 4048)-v, --verbose: Write more information to output-d, --debug: Write even more information to output
Legacy options (still available for compatibility):
--clear: Clear all pixels--fill BRIGHTNESS: Fill with brightness (0-255)--pixel X Y BRIGHTNESS: Set pixel (can be used multiple times)
Coordinates:
- Matrix: 16×16 pixels
- X: 0-15 (left to right)
- Y: 0-15 (top to bottom)
- Brightness: 0-255
Packet Structure:
[Header: 10 bytes][RGB Data: 768 bytes for 16×16]
Header (10 bytes):
- Byte 0:
0x41(Version 1) - Byte 1:
0x00(Flags) - Bytes 2-9:
0x00(Reserved)
RGB Data:
- 3 bytes per pixel (R, G, B)
- Total: 16 × 16 × 3 = 768 bytes
- Order: Row-major (left to right, top to bottom)
- Brightness calculated as:
(R + G + B) / 3
Single Pixel Mode: Send only 3 RGB bytes (total: 13 bytes) to apply the same color to all pixels.
Example (Python):
import socket
# Create DDP packet
header = bytearray([0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
pixels = bytearray([128, 128, 128] * 256) # Fill with mid-brightness
packet = header + pixels
# Send packet
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(packet, ("192.168.178.50", 4048))
sock.close()Use this Home Assistant Integration for easy setup with seamless integration and instant updates via websockets.
Example automation to adjust brightness based on sun position:
configuration.yaml:
rest_command:
obegraensad_brightness_high:
url: 'http://your-server/api/brightness/'
method: PATCH
content_type: 'application/x-www-form-urlencoded'
payload: 'value=100'
obegraensad_brightness_low:
url: 'http://your-server/api/brightness/'
method: PATCH
content_type: 'application/x-www-form-urlencoded'
payload: 'value=1'Automation (Settings → Automations → Edit in YAML):
alias: Obegraensad low brightness
description: ''
triggers:
- trigger: sun
event: sunset
offset: 0
conditions: []
actions:
- action: rest_command.obegraensad_brightness_low
data: {}
mode: singleCreate a second automation or condition to call rest_command.obegraensad_brightness_high at sunrise.
Structure:
src/- Arduino codeplatformio.ini- Build configuration- See OTA Updates section for wireless firmware upload configuration
Structure:
frontend/- Web UI code built with solidjsfrontend/src/- Source filesfrontend/dist/- Built files (generated)
Prerequisites:
This project uses pnpm as the package manager. pnpm is an alternative to npm.
Install pnpm:
# Using npm
npm install -g pnpm
# Using Homebrew (macOS)
brew install pnpm
# Using Corepack (Node.js 16.13+)
corepack enable
corepack prepare pnpm@latest --activateSetup:
cd frontend
pnpm installConfiguration:
Create a .env file in the frontend/ directory and set your device IP:
VITE_DEVICE_IP=192.168.178.50This allows the development server to proxy API requests to your device during development.
Commands:
pnpm dev- Start development server with hot reload (usually athttp://localhost:5173)pnpm build- Build optimized production bundle and generatewebgui.cppfor ESP32pnpm preview- Preview production build locallypnpm lint- Run linter to check code qualitypnpm format- Format code with Prettier
Development Workflow:
- Start the development server:
pnpm dev - Make changes to files in
frontend/src/ - Changes will hot-reload in your browser
- Test your changes against a running device
- When ready, run
pnpm buildto generate the embedded web UI - The build process creates
src/webgui.cppwhich is compiled into the firmware
Docker Build:
If you prefer using Docker instead of installing Node.js and pnpm locally:
docker compose run nodePre-commit Hooks:
# Install pre-commit
pip install pre-commit
# Activate hooks
pre-commit installplugins/MyPlugin.h:
#pragma once
#include "PluginManager.h"
class MyPlugin : public Plugin {
public:
MyPlugin();
~MyPlugin() override;
void setup() override;
void loop() override;
const char* getName() const override;
void teardown() override; // optional
void websocketHook(JsonDocument &request) override; // optional
};plugins/MyPlugin.cpp:
#include "plugins/MyPlugin.h"
MyPlugin::MyPlugin() {
// Constructor logic, if needed
}
void MyPlugin::setup() {
// Setup logic for your plugin
}
void MyPlugin::loop() {
// Loop logic for your plugin
}
const char* MyPlugin::getName() const {
return "MyPlugin"; // name in GUI
}
void MyPlugin::teardown() {
// code if plugin gets deactivated
}
void MyPlugin::websocketHook(JsonDocument &request) {
// handle websocket requests
}#include "plugins/MyPlugin.h"
pluginManager.addPlugin(new MyPlugin());- Verify all soldering points, especially VCC
- Ensure adequate power supply to the board
- Check for loose connections



