The Internet of Things (IoT) is already all around us – from voice‑activated devices to lights, coffee machines, and fridges – and we interact with it everyday in ways we don’t always realize. One of the biggest challenges in scaling IoT devices is device management, particularly over-the-air updates, commonly called OTA.
OTA is critical to deploying new firmware to IoT devices; without it user intervention is required, either by the consumer or by a technician visiting (possibly literally) the device in the field.
This post demonstrates how to use NGINX Open Source or NGINX Plus as an API gateway to deploy OTA updates to devices automatically. (For ease of reading, I’ll refer to NGINX throughout.) I’m using a simple API to handle the firmware versioning (so the devices know which version is the latest) and to deliver the file itself.
For the purposes of this post I’m using an ESP32‑DevkitC device, which uses WiFi for communication. The setup described below applies just as easily to most device types, depending only on the device capabilities and how the firmware is written.
Setting Up the Device
First off, if you haven’t already set up and connected the ESP32‑DevkitC to WiFi, set that up now using the tutorial from Espressif.
Once your device is rocking WiFi, the next step is to load an Arduino sketch with the updated firmware on it. To do this, you need the Arduino IDE, so if you don’t already have it:
- Download the Arduino IDE from the Arduino website, install it, and launch it.
- Navigate to Tools > Manage Libraries, and wait for the Arduino IDE to update.
- Search for and select esp32FOTA to install the software for updating ESP32 firmware over the air.
Using the Arduino IDE, connect to the device via either WiFi or USB and load the following sketch. Be sure to replace serverurl.com
with the domain you are using:
#include <esp32fota.h>
#include <WiFi.h>
const char *ssid = "";
const char *password = "";
esp32FOTA esp32FOTA("esp32-fota-http", 1);
void setup()
{
esp32FOTA.checkURL = "https://serverurl.com/ota";
Serial.begin(115200);
setup_wifi();
}
void setup_wifi()
{
delay(10);
Serial.print("Connecting to ");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
}
void loop()
{
bool updatedNeeded = esp32FOTA.execHTTPcheck();
if (updatedNeeded)
{
esp32FOTA.execOTA();
}
delay(2000);
}
Setting Up an NGINX Instance
Now we have the board set up, it’s time to set up NGINX as an API gateway to handle versioning and delivery to device.
Add the following include
directive to the http
block in nginx.conf:
include /etc/nginx/conf.d/api.conf;
Then create /etc/nginx/conf.d/api.conf with the following contents. Again, be sure to replace serverurl.com
with your domain, in the server_name
and return
directives. I totally recommend that you use HTTPS and have included the directives for configuring it. You need to generate a certificate and key in this case, and specify the paths to them with the ssl_certificate
and ssl_certificate_key
directives.
log_format api_main '$remote_addr - $remote_user [$time_local] "$request"'
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
server {
access_log /var/log/nginx/api_access.log api_main;
listen 443;
server_name serverurl.com;
# TLS config
ssl_certificate ~/fullchain.pem;
ssl_certificate_key ~/privkey.pem;
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
proxy_cookie_domain ~(?P([-0-9a-z]+\.)?[-0-9a-z]+\.[a-z]+)$ "$secure_domain; secure";
ssl_protocols TLSv1.2 TLSv1.1 TLSv1 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!MD5:!DSS;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
location /ota {
return 200 '{"type":"esp32-fota-http", "version": 100, "host": "serverurl.com", "port": 80, "bin": "/esp32-fota-http-100.bin"}';
}
proxy_intercept_errors on;
default_type application/json;
}
The location
block specifies the URI for the API (/ota) and the response that NGINX returns: status code 200
plus the data that the sketch uses, in JSON format. For now it’s hardcoded but in the next section we will make that a little more dynamic, so read on.
Run nginx
-t
to validate the configuration syntax and then run nginx
-s
reload
to reload the config.
Before powering on the device, run a check to make sure the API is working as expected and returns the JSON you’ve loaded into the location
block in api.conf:
curl -X GET https://serverurl.com/ota
{"type":"esp32-fota-http", "version": 100, "host": "serverurl.com", "port": 80, "bin": "/esp32-fota-http-100.bin"}
If you run into any issues, check the api_access.log file and retrace your steps; depending on the device you are using, HTTPS might not be supported.
Automating OTA with the NGINX JavaScript Module
Now we intro a little NGINX JavaScript magic: we are going to write JavaScript code that reads from a folder and updates the output of the API when a new OTA update file is ready for devices to load.
[Editor – This post is one of several that explore use cases for the NGINX JavaScript module. For a complete list, see Use Cases for the NGINX JavaScript Module.]
If you haven’t already installed the NGINX JavaScript module on the NGINX host, see the instructions in the NGINX Plus Admin Guide.
To enable the module, edit nginx.conf once again and add the following load_module
directives in the top‑level (“main”) context:
load_module modules/ngx_http_js_module.so;
load_module modules/ngx_stream_js_module.so;
Edit api.conf and add the following js_include
directive above the server
block defined in Setting Up an NGINX Instance:
js_include ota.js;
In the existing location
block, remove (or comment out) the return
directive and insert the following js_content
directive, then save the file:
js_content ota;
Then create ota.js with the following contents, replacing serverurl.com
with your domain:
function ota(r) {
r.return (200, '{"type":"esp32-fota-http", "version": 100, "host": "serverurl.com", "port": 80, "bin": "/esp32-fota-http-100.bin"}');
}
Save, run nginx
-t
to check the syntax, then run nginx
-s
reload
and the curl
test again.
The final step is to read from the folder where you are storing the firmware files to be updated over the air; make sure this is in a publicly accessible Internet location and substitute the pathname for /path/to/firmware/folder
in the following code.
Edit ota.js once again. We want to keep the same function name (because it’s called inside the location
block in api.conf) but just add a little dynamic coding magic to the mix.
var fs = require('fs');
function ota(r) {
var otaFolder = '/path/to/firmware/folder';
var base = 100;
var fName = 'esp32-fota-http-';
var jsonOut = {};
while (base++){
try{
var file = fs.readFileSync(otaFolder+fName+base+'.bin');
} catch (err) {
base--;
break;
}
}
jsonOut['type'] = 'esp32-fota-http';
jsonOut['version'] = base;
jsonOut['host'] = serverurl.com';
jsonOut['port'] = 80;
jsonOut['bin'] = '/'+fName+base+'.bin';
r.return(200, JSON.stringify(jsonOut));
}
We’re using the Node.js fs module‘s readFileSync
function to find the latest version of the OTA update file in the directory named by otaFolder
. We loop through the filenames in the directory to find the highest version number, using readFileSync
to check if the next file version in the sequence exists. When a version doesn’t exist, the previous (found) version is determined to be the latest. (Note that we’re using readFileSync
because at the time of writing, the NGINX JavaScript module does not support the readdir
function, but only operations on individual files.)
For this to work, filenames must be in the following format:
esp32-fota-http-version.bin
where version is 100 on first version of the file. Increment the version number sequentially for subsequent versions (101, 102, 103, and so on).
Taking Advantage of Advanced Features
There’s a bunch of handy other features in NGINX and devices that we can use too.
For enhanced security with better authentication between device and server, many newer devices come equipped with crypto chips (like the Arduino MKR series) that allow for on‑device x.509 certificate generation (stay tuned for a post on that).
NGINX’s advanced load balancing means you can deploy OTA update files to different server‑based regions (handy if you have country‑specific firmware) or multiple firmware servers or locations.
It’s up to each IoT device to decide when it does updates (or checks for them), so you need to keep older versions of OTA updates available. Devices are sometimes inactive or unable to get Internet access for a period of time, so make sure you keep track of your firmware versions and their dependencies.
OTA increases the longevity of device builds, as it allows you to refine and update device functionality (on the software side anyway). Of course OTA is even more important for things like security upgrades and patches, as it helps create more secure devices.
Just remember that 1 device can turn into 10 can turn into 10,000 before you know it, so it’s critical to have a solid OTA update plan in place before you scale.
To try NGINX Plus, start your free 30-day trial today or contact us to discuss your use cases.