[ELEC][LED] HTTP proxy for infrared LED

Posted on Wed 01 February 2023 in Tech, Hardware

A long time ago, I bought a led strip to pimp my desk and add a bit of backlight behind my screens. The led strip came with a remote, which was (or so I thought) great. However, I ended up never using this led strip ever again, precisely because of this remote. Even if it's not far to reach and works pretty well (as well as an IR remote can work), I still don't use it.

This is a blog entry about the journey I took to reverse the signal sent by the remote, use an ESP32 running an HTTP server and an infrared LED to create an HTTP-controlled LED strip that I can now use from various scripts and shell commands. The journey was actually a bit longer than what is depicted here. Before starting it, I did not even know about ESP32 and the really powerful framework created by Espressif, and started with a plain old and boring Arduino Uno. I will skip this part and go straight to the latest version.

The setup of the Espressif IDE within VSCode was pretty straightforward, however in order to use IRremoteESP8266 I had to make some custom CMakeLists and basically include the Arduino resources as a component. I am definitely not going to recommend doing it that way, but I could not figure out a better way of doing it (the readme only explains it for the Arduino IDE).

Dump everything

The first step was to setup a infrared receiver, point the remote at it and dump the raw data received. This was pretty straightforward and a simple three legged IR receiver connected to the port 5 of my ESP32 did the job, I also had to do a bit a cleanup to remove the noise received but in the end, the code looked like that:

IRrecv irrecv(GPIO_NUM_5);
// do not forget to add the next line in the `app_main` method
// irrecv.enableIRIn();
static esp_err_t desk_led_dump()
{

    ESP_LOGI(TAG, "=========== Dumping IR data for 10 seconds ==========");
    uint32_t start;
    uint32_t current = millis();
    uint32_t capture_time_ms = 1000 * 10;

    for (start = millis(); (current - start) < capture_time_ms; current = millis())
    {
         // Grab an IR code
        if (irrecv.decode(&results))
        {
            uint32_t high = (uint32_t)(results.value >> 32);
            uint32_t low = (uint32_t)(results.value & ULONG_MAX);
            if (results.decode_type == NEC && high != ULONG_MAX)
            {
                ESP_LOGI(TAG, "Got data %X%X", high, low);
            }
            irrecv.resume(); // Prepare for the next value
        }
    }
    ESP_LOGI(TAG, "=========== Done dumping IR data ==========");
    return ESP_OK;
}

There is probably a better way to print a 64 bits values on the ESP32, but this bit shifting worked well and was more than enough for my needs. After a few tries, I was able to dump all the 44 IR codes from my remote, the next step was to setup the circuit that would enable me to send those raw IR code to the LED strip.

It happens that sending an IR signal was actually pretty simple as well, but not as easy as receiving IR data.

Sending raw IR signal

This article from adafruit was more than useful for setting up this part of the project.

The raw code that I will use during this section is 0xFF02FD it powers on/off the led strip, making it pretty easy to debug when working.

Without digging too much into the technical aspect of it (the readme the IR library linked above contains a lot of useful information), we have to setup an IR LED, connected to an NPN transistor and a resistor (what is called, I believe a "driver circuit"), this increases the amount of current provided to the LED. I am not even convinced that this is required as the current provided by the ESP32 should be enough to drive the LED, but I am no electrical engineer and do not want to troubleshoot this kind of issue when doing such projects.

IR LED Emitter diagram

We connect the resistor to the pin 4 of our ESP32 and it is now time to write the software to send an raw (NEC) infrared code to our led strip.

IRsend irsend(GPIO_NUM_4);
// do not forget to add the next line in the `app_main` method
irsend.begin();

irsend.sendNEC(0xFF02FD)

And to my surprise, it worked almost at the first try (I fried the first IR LED that I tried, but using your phone's camera it is pretty easy to see if something is happening or not at the emitter side). With that knowledge I could alreay power on and off my led strip which was the first major milestone of this project.

IR Proxy

The documentation from Espressif regarding the WiFi and HTTP setup was really good, I took one of their example and trimmed it down to a single http handler that would receive an raw hex value that would be emitted by the IR led.

uint32_t strToHex(char str[])
{
    return (uint32_t)strtoul(str, 0, 16);
}

/* An HTTP POST handler */
static esp_err_t desk_led_handler(httpd_req_t *req)
{
    char buf[12];
    int ret, remaining = req->content_len;

    /* Read the data for the request */
    if ((ret = httpd_req_recv(req, buf,
                              MIN(remaining, sizeof(buf)))) <= 0)
    {
        return ESP_FAIL;
    }

    buf[ret] = '\0';
    uint32_t code = strToHex(buf);
    irsend.sendNEC(code);

    /* Log data received */
    ESP_LOGI(TAG, "=========== RECEIVED DATA ==========");
    ESP_LOGI(TAG, "%.*s", ret, buf);
    ESP_LOGI(TAG, "%X", code);
    ESP_LOGI(TAG, "====================================");

    // End response
    httpd_resp_send(req, NULL, 0);
    return ESP_OK;
}

Once again, this is not state of the art, but worked well enough for me to run

curl -vvv  -XPOST http://led.lan/desk  -d 'FF02FD'

in order to power on or off the led strip behind my desk.

Writing a client

I decided (on purpose) to make the code than ran on the ESP32 as simple an stupid as possible, it is far more involve to update and de-reploy it there than changing the client. The HTTP IR proxy will simply forward any value that it receive to the infrared LED, so I created a basic interpreter in python, that could send all the 44 raw code dumped from my remote, it also has a special 'SLEEP' instruction that I can use to wait between to commands. The final usage of the script looks like that

led.py POWER # emulate power button
led.py POWER ORANGE SLEEP SLEEP YELLOW POWER POWER # Set the color to orange, wait a bit, then set it to yellow and turn it off and then on (+- blink)
led YELLOW 5000 SLEEP POWER SLEEP POWER # Turn the LED yellow, then sleep for 5 seconds, power off, sleep and power on

The code is super simple:

import sys
import requests
from time import sleep
from buttons import Button


# The name of the button is valid, plus any number
SLEEP_TOKEN = "SLEEP"
TOKENS = list(map(lambda x: x[0], Button.__members__.items())) + [SLEEP_TOKEN]


def send_button(button: Button):
    requests.post(
        "http://led.lan/desk",
        data=hex(button.value).removeprefix("0x") + "\n",
    )


def validate_program(program: list[str]):
    for token in program:
        is_valid_button_token = token in TOKENS
        is_valid_timing_token = token.isnumeric()

        if not is_valid_button_token and not is_valid_timing_token:
            return token

    return None


if __name__ == "__main__":
    program = []
    if len(sys.argv) == 1:
        raise Exception(
            "No parameter given, please load a file using -f or use the command line to pass parameters"
        )

    if len(sys.argv) == 2 and sys.argv[1] == "-h":
        for token in TOKENS:
            print(token)
        sys.exit(0)

    if len(sys.argv) == 3 and sys.argv[1] == "-f":
        program = open(sys.argv[2], "r").read().split()

    else:
        program = sys.argv[1:]

    error_token = validate_program(program)
    if error_token:
        raise Exception(f"{error_token} is not a valid token")

    sleep_delay_ms = 250
    for current, token in enumerate(program):
        #  Any number will be interpreted as the new sleep delay, in milliseconds
        if token.isnumeric():
            new_sleep = int(token)
            sleep_delay_ms = int(token)
        elif token == SLEEP_TOKEN:
            sleep(sleep_delay_ms / 1000)
        else:
            send_button(Button[token])
            # we don't want to execute the last sleep
            if current != len(program) - 1:
                sleep(sleep_delay_ms / 1000)

It is also possible to load a "led script" using the -f parameter. I can now have a cronjob that turn the led on, set them in blue a blink a few time in order to remind me to drink some water when spendng hours in front of my computer, this was definitely worth it.