Skip to content

Using an ESP32 as a REST server with Neopixels.

Posted on:September 17, 2023 at 10:57 AM

TL:DR

Checkout the code here. I like to think it’s pretty well commented so that very well may be enough for most people, as this isn’t that complicated of a project to begin with. I used the ArduinoJson, WebServer, and Wifi libraries to make the ESP32 accept API requests. I also utilized FreeRTOS to use one core (turns out the ESP32 is a dual core processor) to handle the Neopixel lighting, while the other core handles other tasks. This fixes many problems that are often had using Neopixels and the neopixel library with an ESP32. This allows control of the lights from any device that can send http requests (computer, phone, another micro-controller, etc.). Combine this with OTA updates and we’ve got a pretty fun and adjustable system for lighting. Final result:

Finished project as utilized by me

We have more potential here, so stay tuned since I’m going to add some sensors for more useful GET requests.

In more detail please…

Okay if you’re interested, there’s more explaining to do as well as more to “why” you might do this. There are other tutorials about getting graphic interfaces for controlling lights with an ESP32, but I realized that I would really only want 5 or 6 different lighting “modes” max for typical use. I also didn’t want to be limited to the interface that would be suppliable by the ESP32. By making the ESP32 act as a REST API server, I could write a front end in any language, control the lights from another micro-controller based on environmental factors, or (as I most often do) use keyboard shortcuts or a simple icon on my phone to set the lighting quickly without loading any interfaces.

A Quick Note on Hardware

The hardware is the easy part here so I’m not really going to cover it in full depth. I used 5V neopixesl so I could use a 5V power supply to power both the neopixels and the ESP32, Just make sure to pick a supply with enough juice for the number of neopixels you’re using. Lastly just hook the neopixels up to one of the ESP32s pins. I used pin 27 in my code. If you need more background on how to use neopixels, check out adafruit’s neopixel guide.

REST API… on a Microcontroller

Yes… well maybe. I should probably call it REST-like. The truth is I’m a mechanical guy, I don’t understand the REST architecture quite well enough to say if this implementation fully satisfies those requirements, and the truth is that it probably doesn’t. That said you can interact with it’s endpoints in the same way you would a RESTful API so that’s what I’m calling it for simplicities sake. This was the first requirement for this project and the core parts of the code look like this.

Connectivity and Handling Requests

To make the REST-like API happen, we’ll need:

To handle that we’re going to program the ESP32 using the arduino interface, and lean on these 3 libraries

#include <ArduinoJson.h>
#include <WebServer.h>
#include <WiFi.h>

If you’ve never used the ardiuno IDE with an ESP32 before, you’ll need to do some setup. Follow this guide and you should be ready to continue. I’ll assume that setting up WiFi is trivial as there are plenty of tutorials on that so I’ll focus on the second two requirements.

First for the formatting of requests and responses. You could probably handle this a few different ways, but what I’m used to is JSON, and fortunately that’s pretty easy. We’ll set up a response to have 3 fields per data point which looks something like this.

// setting up a JSON doc for sending data
StaticJsonDocument<250> jsonDocument;
char buffer[250];

// Framework for JSON document to send
void create_json(char *tag, float value, char *unit) {
  jsonDocument.clear();
  jsonDocument["type"] = tag;
  jsonDocument["value"] = value;
  jsonDocument["unit"] = unit;
  serializeJson(jsonDocument, buffer);
}

void add_json_object(char *tag, float value, char *unit) {
  JsonObject obj = jsonDocument.createNestedObject();
  obj["type"] = tag;
  obj["value"] = value;
  obj["unit"] = unit;
}

This gives us a format for receiving data back from a GET request! The same ArduinoJson library will also help us handle out POST requests, but first, let’s get that set up.

To route requests, we rely mostly on the WebServer library. The code that does the actual routing looks like this:

void setup_routing() {
  server.on("/data", getData);
  server.on("/led", HTTP_POST, handlePost);

  server.begin();
}

This specifies a url in the format of a subfolder, optionally an HTTP method, and a function to run. In essence sending a request to http://{IP_Address_of_ESP32}/data will run the getData function. Likewise a request sent to http://{IP_Address_of_ESP32}/led must be a POST request and will run the function handlePost. Now we need to write those functions.

Post requests

This is the more complicated of the two in the sense that we haven’t done much to set it up yet

void handlePost() {
  if (server.hasArg("plain") == false) {
  }
  String body = server.arg("plain");
  deserializeJson(jsonDocument, body);

  // Change has been made so initialize change_canary
  change_canary = 0;

  // This checks if there is a mode assigned to the number sent. If not no change is made
  if (jsonDocument["mode"] < (sizeof(lighting_names)/sizeof(lighting_names[0]))) {
     server.send(200, "application/json", "{Success!}");
     mode_value = jsonDocument["mode"];
  } else {
     server.send(404, "application/json", "{Error: 404}");
     // Maybe put an error mode in here?
  }
}

The first 3 lines of this code just handle making the request readable and usable by taking the JSON info, and putting into a format we can access. Then we have a variable called change_canary. This variable will be used to detect when we’ve made a change to the lighting mode. Then we check to see what we’ve received. The ESP32 will be expecting one parameter called mode and it should be a number. Each of these numbers will correspond to a pre-set lighting mode, the names of which are stored in a lighting_names array. We check to see if the mode is valid, and we send a 200 response if it is and a 404 response if it isn’t.

Get Requests

This one is pretty simple. All we can give in response right now is the lighting mode that is currently active. We simply add the current mode value and name of the lighting mode to our pre-structured JSON document along with a title for the type of data we’re sending. Then we package that up, and send it on back. All in all, it’s only 5 lines of code:

void getData() {
  Serial.println("Get data");
  jsonDocument.clear();
  add_json_object("Lighting", mode_value, lighting_names[mode_value]);
  serializeJson(jsonDocument, buffer);
  server.send(200, "application/json", buffer);
}

Dual Core Processing

I had done a couple of small projects with an ESP32, but honestly I was shocked to find out that the standard ESP32 actually has 2 seperate cores. In addition to helping us to simultaneously handle the API server and the lighting, many people actually find it necessary to utilized the second core for lighting. When attempting to run the neopixels without specifying which core to run them on, I would run into errors where the neopixels would flash various colors seemingly without cause. Fortunately purposeful utilization of the cores is not complicated and beneficial even if it wasn’t for the lighting issues. The code that specifies to run the lights on the second core looks like this:

void setup_lights() {
  xTaskCreatePinnedToCore(
    run_lights,               // Function that should be called
    "Continuous Lighting",    // Name of the task (for debugging)
    1500,                     // Stack size (bytes)
    NULL,                     // Parameter to pass
    1,                        // Task priority
    NULL,                     // Task handle
    1                         // Core to run on (0 or 1)
  );
}

And that’s pretty much all we need. FreeRTOS comes by default when using ESP32 on Arduino so this little block of code is all we need.

Lighting and bringing it all together

The function to handle lighting is pretty easy now that we’ve got everything else set up. In the section above we told the second core to run a function called run_lights so creating that function we’ll have something like this:

void run_lights(void * parameter) {
  for (;;) {
    if (mode_value == 0) {
      noLights();
    } else if (mode_value == 1) {
      pandoraWave();
    } else if (mode_value == 2) {
      lightsOn();
    } else if (mode_value == 3) {
      purpleWave();
    } else if (mode_value == 4) {
      fire();
    }
    if (WiFi.status() != WL_CONNECTED) {
      reconnectWiFi();
    }
    // Pause the task again for 500ms
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}

This is a pretty straightforward function. Simply put it’s an infinite loop that checks what “mode” the lights are in and runs the appropriate function. These functions can be a solid color, or animation your heart desires (or can program). Look at the full (code)[link] to see my examples, but go crazy. This function also makes sure your wifi will reconnect if your router drops out for a while, preventing the need for a hard reset in those scenarios.

The final details are pretty basic Arduino sketch stuff, such as starting the wifi, setting up the serial monitor, the main function, and things such as that. I did go ahead and make it easy to update by adding over-the-air (OTA) capabilities, but that is pretty simple and there are plenty of tutorials about that elsewhere.

Bonus: Protecting Passwords from Git Repositories!

One last bit is that it may be confusing the way that I handled wifi and OTA update passwords if you haven’t encountered this before. The long and short of it is, I don’t want to publish my passwords to a git repository. To fix this, we simply import a file with something like this in it:

#define SECRET_SSID "MyWiFiSSID"
#define SECRET_PASSWORD "MySecretPassword"
#define OTA_MD5_Hash "SomeMD5HashOfAPassword"

You can then just import this file to you main file by adding a line such as #include "customLightingSecrets.h" to the top of the file. The just reference the above “secrets” the same as you would any variable. Add the secrets file to your .gitignore and your golden! I include an example file in my git repository to make it easy.

Wrapping Up…

All in all, a pretty easy and pretty fun project. i’ve found this a great and super flexible way to control some fun lighting in my office and it’s easy to trigger automatically with certain events such as firing up a computer, or even something as simple as getting home. If you thought this was interesting, stay tuned because I plan to add a sensor so we can get more out of this same set up when making GET requests. Thanks for reading!

Full Code