Back in undergrad, when I set up the LED strip around my apartment’s living room window, I thought the little IR remote it came with was fine. Point, click, color changes. Simple.
Then I lost the remote in the couch cushions for the third time and decided this was now an engineering problem.
The LED strip itself is a 120V outdoor-rated, waterproofed unit. Cracking into the controller to directly hack the hardware was off the table; I like my apartment un-scorched and my fingers un-electrocuted. But the IR remote? That’s just blasting NEC-encoded signals into the void. Nothing stopping me from doing the same thing with an ESP8266 and a cheap IR LED.
So that’s what I did. A small networked device sat next to the LED strip controller, listened for HTTP requests over Wi-Fi, and fired off the corresponding IR commands. Everything stayed on the local network, and the API was plain enough that anything in the apartment could control the lights.
Hardware
The hardware side of this project is refreshingly simple compared to some of my other hacks.
- ESP8266 (NodeMCU) as the usual suspect for Wi-Fi-enabled microcontroller projects
- IR LED on pin
D2with a 10-ohm series resistor - Status LED on pin
D6with a 1k-ohm resistor to ground
That’s basically it. The status LED blinks every 500ms to let me know the device is alive and connected. Point the IR LED at the strip’s receiver and go.
The boot pin configuration (D3, D4, D8) follows the standard ESP8266 pull-up/pull-down setup. Nothing fancy but worth documenting so future-me doesn’t spend an hour wondering why the board won’t flash.
IR Protocol
The LED strip’s remote uses NEC encoding, which is one of the more common IR protocols out there. Each command is a 32-bit transmission with a fixed address byte (0x00) and a variable command byte.
I mapped out every button on the remote to its corresponding hex code:
| Function | Command | HEX |
|---|---|---|
| Power On | ON | 0x07 |
| Power Off | OFF | 0x06 |
| Brightness Up | UP | 0x05 |
| Brightness Down | DOWN | 0x04 |
| White | COLOR | 0x08 |
| Red | COLOR | 0x09 |
| Green | COLOR | 0x0C |
| Blue | COLOR | 0x10 |
| Flash | FUNCTION | 0x0F |
| Strobe | FUNCTION | 0x17 |
| Fade | FUNCTION | 0x13 |
| Smooth | FUNCTION | 0x1B |
There are 16 colors total (including some fun ones like “pea green” and “dark orchid”) and four special functions. The IRremoteESP8266 library handles all the heavy lifting for encoding and transmission.
void led_send_value(uint8_t value) {
irsend.sendNEC(irsend.encodeNEC(LED_ADDRESS, value));
}
One line. Beautiful.
REST API
Rather than just replicating the remote’s buttons, I built a proper REST API that maps each control surface to its own endpoint. The ESP8266 runs a lightweight HTTP server on port 80 with the following routes:
| Route | Method | Parameters | Purpose |
|---|---|---|---|
/ | GET | API documentation and available routes | |
/power | POST | power=on|off | Power control |
/color | POST | color=<name> | Set color (16 options) |
/brightness | POST | brightness=up|down | Adjust brightness (9 steps) |
/function | POST | function=flash|strobe|fade|smooth | Special effects |
/raw | POST | raw=<byte> | Send a raw NEC command (for debugging) |
/cached-state | GET | Last commanded values |
Every endpoint validates its input and returns a plain-text status message. Each one also answers GET as well as POST, and takes a documentation=true parameter that returns usage information. I wanted this to be something I could poke at with curl from a terminal without needing to remember anything.
curl -X POST "http://window-leds.local/color?color=blue"
Blue window. Done.
State Caching
The service maintains a cache of the last commanded values for power, color, brightness, and function mode. You can query it at /cached-state to see what the device thinks the LED strip is doing.
The key word there is “thinks.” IR communication is one-way. The LED strip has no way to report its actual state back to the ESP8266. If someone walks up and hits a button on the physical remote (assuming they found it in the couch cushions; thank you roommate), the cache goes stale and the service has no idea.
This is an acknowledged limitation and honestly, I was fine with it. The physical remote kept working alongside the network control. I didn’t want to break that compatibility; it’s one of the reasons I went with IR replication instead of hacking the controller directly.
Network Setup
For Wi-Fi, I used the WiFiManager library. On first boot, the ESP8266 creates an access point called Window_LEDs_AP with a captive portal. Connect to it, enter your Wi-Fi credentials, and you’re done. It remembers the network and auto-connects on subsequent boots.
I also enabled mDNS so the device registers itself as window-leds.local on the network. This means I don’t need to remember or look up the IP address every time my router decides to shuffle things around. http://window-leds.local and I’m in.
void station_start() {
wifiManager.autoConnect(AP_NAME, AP_PASS);
if (MDNS_ENABLED) {
MDNS.begin(MDNS_HOSTNAME);
}
rest_service_start();
}
There’s also some connection monitoring logic. If the Wi-Fi drops, the station module detects it, tears down the REST service, flips back to AP mode for reconfiguration, and restarts everything when connectivity returns. Nothing clever, but it means I don’t have to walk over and power-cycle the device when the router hiccups.
Architecture
The codebase is organized into small modules, each with a clear responsibility:
LED-Service.ino: entry point and main loopHardwareConfig.h: GPIO pin definitions and hardware initializationLEDStrip.h/cpp: IR protocol layer (NEC encoding, command mapping)RestService.h/cpp: HTTP server and API endpoint handlersStation.h/cpp: Wi-Fi management, mDNS, service lifecycleNetworkConfig.h: port, hostname, and AP configuration
I’m pretty happy with how cleanly the concerns separated out here: the REST service knows nothing about NEC encoding, the IR layer knows nothing about HTTP, and the station module just manages connectivity.
Quirks and Findings
A few things I discovered along the way that are worth noting:
Brightness is context-dependent. When the strip is in a static color mode, the brightness up/down commands adjust LED intensity across 9 steps. But when you’re in one of the special function modes (flash, strobe, fade, smooth), the same brightness commands adjust the transition speed instead. The API handles both cases transparently.
Special functions have hidden behaviors. Pressing the same function button twice does something different than pressing it once:
- Flash twice = Strobe effect
- Strobe twice = Smooth fade through brightness
- Fade twice = RGB single-color cycling
- Smooth twice = RGB single-color flashing
I documented these in the code comments but didn’t expose them as separate API endpoints. The /function endpoint just fires the command; if you want the double-press behavior, hit it twice.
Final Thoughts
This project slotted into my undergrad home automation setup as one of the pieces that just worked. Everything ran on the local network, nothing phoned home, and there was no app to install or subscription to forget about. Any device in the apartment could control the window LEDs with an HTTP request; a curl command from my laptop or a shortcut on my phone did the job. The rig stayed behind when undergrad ended, so this is a post about an old thing.
It was also one of those rare projects where the hardware was trivial, the software was straightforward, and the whole thing worked. No teardown photos of a destroyed enclosure this time, no signal-extraction detective work. An IR LED, an ESP8266, and a REST API.
I lost a remote, so I replaced it with an entire web server. As one does.
You can find the source code on GitHub.
Thanks for reading. Stay tuned and keep building.