BLE Server and Client (Bluetooth Low Energy)
Learn how to make a BLE (Bluetooth Low Energy) connection between two ESP32 boards. One ESP32 is going to be the server, and the other ESP32 will be the client. The BLE server advertises characteristics that contain sensor readings that the client can read. The ESP32 BLE client reads the values of those characteristics (temperature and humidity) and displays them on an OLED display.
Recommended Reading: Getting Started with ESP32 Bluetooth Low Energy (BLE)
What is Bluetooth Low Energy?
Before going straight to the project, it is important to take a quick look at some essential BLE concepts so that you're able to better understand the project later on. If you're already familiar with BLE, you can skip to the Project Overview section.
Bluetooth Low Energy, BLE for short, is a power-conserving variant of Bluetooth. BLE's primary application is short-distance transmission of small amounts of data (low bandwidth). Unlike Bluetooth that is always on, BLE remains in sleep mode constantly except for when a connection is initiated.
This makes it consume very low power. BLE consumes approximately 100x less power than Bluetooth (depending on the use case). You can check the main differences between Bluetooth and Bluetooth Low Energy here.
BLE Server and Client
With Bluetooth Low Energy, there are two types of devices: the server and the client. The ESP32 can act either as a client or as a server.
The server advertises its existence, so it can be found by other devices and contains data that the client can read. The client scans the nearby devices, and when it finds the server it is looking for, it establishes a connection and listens for incoming data. This is called point-to-point communication.
There are other possible communication modes like broadcast mode and mesh network (not covered in this tutorial).
GATT
GATT stands for Generic Attributes and it defines a hierarchical data structure that is exposed to connected BLE devices. This means that GATT defines the way that two BLE devices send and receive standard messages. Understanding this hierarchy is important because it will make it easier to understand how to use BLE with the ESP32.
Profile: standard collection of services for a specific use case;
Service: collection of related information, like sensor readings, battery level, heart rate, etc. ;
Characteristic: it is where the actual data is saved on the hierarchy (value);
Descriptor: metadata about the data;
Properties: describe how the characteristic value can be interacted with. For example: read, write, notify, broadcast, indicate, etc.
In our example, we'll create a service with two characteristics. One for the temperature and another for the humidity. The actual temperature and humidity readings are saved on the value under their characteristics. Each characteristic has the notify property, so that it notifies the client whenever the values change.
UUID
Each service, characteristic, and descriptor have a UUID (Universally Unique Identifier). A UUID is a unique 128-bit (16 bytes) number. For example:
55072829-bc9e-4c53-938a-74a6d4c78776
There are shortened UUIDs for all types, services, and profiles specified in the SIG (Bluetooth Special Interest Group).
But if your application needs its own UUID, you can generate it using this UUID generator website.
In summary, the UUID is used for uniquely identifying information. For example, it can identify a particular service provided by a Bluetooth device.
For a more detailed introduction about BLE, read our getting started guide:
Getting Started with ESP32 Bluetooth Low Energy (BLE) on Arduino IDE
Project Overview
In this tutorial, you're going to learn how to make a BLE connection between two ESP32 boards. One ESP32 is going to be the BLE server, and the other ESP32 will be the BLE client.
The ESP32 BLE server is connected to a BME280 sensor and it updates its temperature and humidity characteristic values every 30 seconds.
The ESP32 client connects to the BLE server and it is notified of its temperature and humidity characteristic values. This ESP32 is connected to an OLED display and it prints the latest readings.
This project is divided into two parts:
Part 1 ESP32 BLE server
Part 2 ESP32 BLE client
Parts Required
Here's a list of the parts required to follow this project:
ESP32 BLE Server:
ESP32 DOIT DEVKIT V1 Board (read Best ESP32 development boards)
BME280 Sensor
Jumper wires
Breadboard
Smartphone with Bluetooth (optional)
ESP32 BLE Client:
ESP32 DOIT DEVKIT V1 Board (read Best ESP32 development boards)
OLED display
Jumper wires
Breadboard
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
1) ESP32 BLE Server
In this part, we'll set up the BLE Server that advertises a service that contains two characteristics: one for temperature and another for humidity. Those characteristics have the Notify property to notify new values to the client.
Schematic Diagram
The ESP32 BLE server will advertise characteristics with temperature and humidity from a BME280 sensor. You can use any other sensor as long as you add the required lines in the code.
We're going to use I2C communication with the BME280 sensor module. For that, wire the sensor to the default ESP32 SCL (GPIO 22) and SDA (GPIO 21) pins, as shown in the following schematic diagram.
Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
Installing BME280 Libraries
As mentioned previously, we'll advertise sensor readings from a BME280 sensor. So, you need to install the libraries to interface with the BME280 sensor.
Adafruit_BME280 library
Adafruit_Sensor library
You can install the libraries using the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the library name.
Installing Libraries (VS Code + PlatformIO)
If you're using VS Code with the PlatformIO extension, copy the following to the platformio.ini file to include the libraries.
lib_deps = adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/Adafruit BME280 Library @ ^2.1.2
ESP32 BLE Server Code
With the circuit ready and the required libraries installed, copy the following code to the Arduino IDE, or to the main.cpp file if you're using VS Code.
/*********
Rui Santos
Complete instructions at https://RandomNerdTutorials.com/esp32-ble-server-client/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
//Default Temperature is in Celsius
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius
//BLE server name
#define bleServerName "BME280_ESP32"
Adafruit_BME280 bme; // I2C
float temp;
float tempF;
float hum;
// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
bool deviceConnected = false;
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "91bad492-b950-4226-aa2b-4ede9fa42f59"
// Temperature Characteristic and Descriptor
#ifdef temperatureCelsius
BLECharacteristic bmeTemperatureCelsiusCharacteristics("cba1d466-344c-4be3-ab3f-189f80dd7518", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeTemperatureCelsiusDescriptor(BLEUUID((uint16_t)0x2902));
#else
BLECharacteristic bmeTemperatureFahrenheitCharacteristics("f78ebbff-c8b7-4107-93de-889a6a06d408", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeTemperatureFahrenheitDescriptor(BLEUUID((uint16_t)0x2902));
#endif
// Humidity Characteristic and Descriptor
BLECharacteristic bmeHumidityCharacteristics("ca73b3ba-39f6-4ab3-91ae-186dc9577d99", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeHumidityDescriptor(BLEUUID((uint16_t)0x2903));
//Setup callbacks onConnect and onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
void setup() {
// Start serial communication
Serial.begin(115200);
// Init BME Sensor
initBME();
// Create the BLE Device
BLEDevice::init(bleServerName);
// Create the BLE Server
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Create the BLE Service
BLEService *bmeService = pServer->createService(SERVICE_UUID);
// Create BLE Characteristics and Create a BLE Descriptor
// Temperature
#ifdef temperatureCelsius
bmeService->addCharacteristic(&bmeTemperatureCelsiusCharacteristics);
bmeTemperatureCelsiusDescriptor.setValue("BME temperature Celsius");
bmeTemperatureCelsiusCharacteristics.addDescriptor(&bmeTemperatureCelsiusDescriptor);
#else
bmeService->addCharacteristic(&bmeTemperatureFahrenheitCharacteristics);
bmeTemperatureFahrenheitDescriptor.setValue("BME temperature Fahrenheit");
bmeTemperatureFahrenheitCharacteristics.addDescriptor(&bmeTemperatureFahrenheitDescriptor);
#endif
// Humidity
bmeService->addCharacteristic(&bmeHumidityCharacteristics);
bmeHumidityDescriptor.setValue("BME humidity");
bmeHumidityCharacteristics.addDescriptor(new BLE2902());
// Start the service
bmeService->start();
// Start advertising
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pServer->getAdvertising()->start();
Serial.println("Waiting a client connection to notify...");
}
void loop() {
if (deviceConnected) {
if ((millis() - lastTime) > timerDelay) {
// Read temperature as Celsius (the default)
temp = bme.readTemperature();
// Fahrenheit
tempF = 1.8*temp +32;
// Read humidity
hum = bme.readHumidity();
//Notify temperature reading from BME sensor
#ifdef temperatureCelsius
static char temperatureCTemp[6];
dtostrf(temp, 6, 2, temperatureCTemp);
//Set temperature Characteristic value and notify connected client
bmeTemperatureCelsiusCharacteristics.setValue(temperatureCTemp);
bmeTemperatureCelsiusCharacteristics.notify();
Serial.print("Temperature Celsius: ");
Serial.print(temp);
Serial.print(" oC");
#else
static char temperatureFTemp[6];
dtostrf(tempF, 6, 2, temperatureFTemp);
//Set temperature Characteristic value and notify connected client
bmeTemperatureFahrenheitCharacteristics.setValue(temperatureFTemp);
bmeTemperatureFahrenheitCharacteristics.notify();
Serial.print("Temperature Fahrenheit: ");
Serial.print(tempF);
Serial.print(" oF");
#endif
//Notify humidity reading from BME
static char humidityTemp[6];
dtostrf(hum, 6, 2, humidityTemp);
//Set humidity Characteristic value and notify connected client
bmeHumidityCharacteristics.setValue(humidityTemp);
bmeHumidityCharacteristics.notify();
Serial.print(" - Humidity: ");
Serial.print(hum);
Serial.println(" %");
lastTime = millis();
}
}
}
View raw code
You can upload the code, and it will work straight away advertising its service with the temperature and humidity characteristics. Continue reading to learn how the code works, or skip to the Client section.
There are several examples showing how to use BLE with the ESP32 in the Examples section. In your Arduino IDE, go to File > Examples > ESP32 BLE Arduino. This server sketch is based on the Notify example.
Importing Libraries
The code starts by importing the required libraries.
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
Choosing Temperature Unit
By default, the ESP sends the temperature in Celsius degrees. You can comment the following line or delete it to send the temperature in Fahrenheit degrees.
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius
BLE Server Name
The following line defines a name for our BLE server. Leave the default BLE server name. Otherwise, the server name in the client code also needs to be changed (because they have to match).
//BLE server name
#define bleServerName "BME280_ESP32"
BME280 Sensor
Create an Adafruit_BME280 object called bme on the default ESP32 I2C pins.
Adafruit_BME280 bme; // I2C
The temp, tempF and hum variables will hold the temperature in Celsius degrees, the temperature in Fahrenheit degrees, and the humidity read from the BME280 sensor.
float temp;
float tempF;
float hum;
Other Variables
The following timer variables define how frequently we want to write to the temperature and humidity characteristic. We set the timerDelay variable to 30000 milliseconds (30 seconds), but you can change it.
// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
The deviceConnected boolean variable allows us to keep track if a client is connected to the server.
bool deviceConnected = false;
BLE UUIDs
In the next lines, we define UUIDs for the service, for the temperature characteristic in celsius, for the temperature characteristic in Fahrenheit, and for the humidity.
// https://www.uuidgenerator.net/
#define SERVICE_UUID "91bad492-b950-4226-aa2b-4ede9fa42f59"
// Temperature Characteristic and Descriptor
#ifdef temperatureCelsius
BLECharacteristic bmeTemperatureCelsiusCharacteristics("cba1d466-344c-4be3-ab3f-189f80dd7518", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeTemperatureCelsiusDescriptor(BLEUUID((uint16_t)0x2902));
#else
BLECharacteristic bmeTemperatureFahrenheitCharacteristics("f78ebbff-c8b7-4107-93de-889a6a06d408", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeTemperatureFahrenheitDescriptor(BLEUUID((uint16_t)0x2901));
#endif
// Humidity Characteristic and Descriptor
BLECharacteristic bmeHumidityCharacteristics("ca73b3ba-39f6-4ab3-91ae-186dc9577d99", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeHumidityDescriptor(BLEUUID((uint16_t)0x2903));
I recommend leaving all the default UUIDs. Otherwise, you also need to change the code on the client sideso the client can find the service and retrieve the characteristic values.
setup()
In the setup(), initialize the Serial Monitor and the BME280 sensor.
// Start serial communication
Serial.begin(115200);
// Init BME Sensor
initBME();
Create a new BLE device with the BLE server name you've defined earlier:
// Create the BLE Device
BLEDevice::init(bleServerName);
Set the BLE device as a server and assign a callback function.
// Create the BLE Server
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
The callback function MyServerCallbacks() changes the boolean variable deviceConnected to true or false according to the current state of the BLE device. This means that if a client is connected to the server, the state is true. If the client disconnects, the boolean variable changes to false. Here's the part of the code that defines the MyServerCallbacks() function.
//Setup callbacks onConnect and onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};
Start a BLE service with the service UUID defined earlier.
BLEService *bmeService = pServer->createService(SERVICE_UUID);
Then, create the temperature BLE characteristic. If you're using Celsius degrees it sets the following characteristic and descriptor:
#ifdef temperatureCelsius
bmeService->addCharacteristic(&bmeTemperatureCelsiusCharacteristics);
bmeTemperatureCelsiusDescriptor.setValue("BME temperature Celsius");
bmeTemperatureCelsiusCharacteristics.addDescriptor(new BLE2902());
Otherwise, it sets the Fahrenheit characteristic:
#else
bmeService->addCharacteristic(&dhtTemperatureFahrenheitCharacteristics);
bmeTemperatureFahrenheitDescriptor.setValue("BME temperature Fahrenheit");
bmeTemperatureFahrenheitCharacteristics.addDescriptor(new BLE2902());
#endif
After that, it sets the humidity characteristic:
// Humidity
bmeService->addCharacteristic(&bmeHumidityCharacteristics);
bmeHumidityDescriptor.setValue("BME humidity");
bmeHumidityCharacteristics.addDescriptor(new BLE2902());
Finally, you start the service, and the server starts the advertising so other devices can find it.
// Start the service
bmeService->start();
// Start advertising
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pServer->getAdvertising()->start();
Serial.println("Waiting a client connection to notify...");
loop()
The loop() function is fairly straightforward. You constantly check if the device is connected to a client or not. If it's connected, and the timerDelay has passed, it reads the current temperature and humidity.
if (deviceConnected) {
if ((millis() - lastTime) > timerDelay) {
// Read temperature as Celsius (the default)
temp = bme.readTemperature();
// Fahrenheit
tempF = temp*1.8 +32;
// Read humidity
hum = bme.readHumidity();
If you're using temperature in Celsius it runs the following code section. First, it converts the temperature to a char variable (temperatureCTemp variable). We must convert the temperature to a char variable type to use it in the setValue() function.
static char temperatureCTemp[6];
dtostrf(temp, 6, 2, temperatureCTemp);
Then, it sets the bmeTemperatureCelsiusCharacteristic value to the new temperature value (temperatureCTemp) using the setValue() function. After settings the new value, we can notify the connected client using the notify() function.
//Set temperature Characteristic value and notify connected client
bmeTemperatureCelsiusCharacteristics.setValue(temperatureCTemp);
bmeTemperatureCelsiusCharacteristics.notify();
We follow a similar procedure for the Temperature in Fahrenheit.
#else
static char temperatureFTemp[6];
dtostrf(f, 6, 2, temperatureFTemp);
//Set temperature Characteristic value and notify connected client
bmeTemperatureFahrenheitCharacteristics.setValue(tempF);
bmeTemperatureFahrenheitCharacteristics.notify();
Serial.print("Temperature Fahrenheit: ");
Serial.print(tempF);
Serial.print(" *F");
#endif
Sending the humidity also uses the same process.
//Notify humidity reading from DHT
static char humidityTemp[6];
dtostrf(hum, 6, 2, humidityTemp);
//Set humidity Characteristic value and notify connected client
bmeHumidityCharacteristics.setValue(humidityTemp);
bmeHumidityCharacteristics.notify();
Serial.print(" - Humidity: ");
Serial.print(hum);
Serial.println(" %");
Testing the ESP32 BLE Server
Upload the code to your board and then, open the Serial Monitor. It will display a message as shown below.
Then, you can test if the BLE server is working as expected by using a BLE scan application on your smartphone like nRF Connect. This application is available for Android and iOS.
After installing the application, enable Bluetooth on your smartphone. Open the nRF Connect app and click on the Scan button. It will find all Bluetooth nearby devices, including your BME280_ESP32 device (it is the BLE server name you defined on the code).
Connect to your BME280_ESP32 device and then, select the client tab (the interface might be slightly different). You can check that it advertises the service with the UUID we defined in the code, as well as the temperature and humidity characteristics. Notice that those characteristics have the Notify property.
Your ESP32 BLE Server is ready!
Go to the next section to create an ESP32 client that connects to the server to get access to the temperature and humidity characteristics and get the readings to display them on an OLED display.
2) ESP32 BLE Client
In this section, we'll create the ESP32 BLE client that will establish a connection with the ESP32 BLE server, and display the readings on an OLED display.
Schematic
The ESP32 BLE client is connected to an OLED display. The display shows the readings received via Bluetooth.
Wire your OLED display to the ESP32 by following the next schematic diagram. The SCL pin connects to GPIO 22 and the SDA pin to GPIO 21.
Installing the SSD1306, GFX and BusIO Libraries
You need to install the following libraries to interface with the OLED display:
Adafruit_SSD1306 library
Adafruit GFX library
Adafruit BusIO library
To install the libraries, go Sketch> Include Library > Manage Libraries, and search for the libraries' names.
Installing Libraries (VS Code + PlatformIO)
If you're using VS Code with the PlatformIO extension, copy the following to the platformio.ini file to include the libraries.
lib_deps =
adafruit/Adafruit GFX Library@^1.10.12
adafruit/Adafruit SSD1306@^2.4.6
ESP32 BLE Client Code
Copy the BLE client Sketch to your Arduino IDE or to the main.cpp file if you're using VS Code with PlatformIO.
/*********
Rui Santos
Complete instructions at https://RandomNerdTutorials.com/esp32-ble-server-client/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
#include "BLEDevice.h"
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
//Default Temperature is in Celsius
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius
//BLE Server name (the other ESP32 name running the server sketch)
#define bleServerName "BME280_ESP32"
/* UUID's of the service, characteristic that we want to read*/
// BLE Service
static BLEUUID bmeServiceUUID("91bad492-b950-4226-aa2b-4ede9fa42f59");
// BLE Characteristics
#ifdef temperatureCelsius
//Temperature Celsius Characteristic
static BLEUUID temperatureCharacteristicUUID("cba1d466-344c-4be3-ab3f-189f80dd7518");
#else
//Temperature Fahrenheit Characteristic
static BLEUUID temperatureCharacteristicUUID("f78ebbff-c8b7-4107-93de-889a6a06d408");
#endif
// Humidity Characteristic
static BLEUUID humidityCharacteristicUUID("ca73b3ba-39f6-4ab3-91ae-186dc9577d99");
//Flags stating if should begin connecting and if the connection is up
static boolean doConnect = false;
static boolean connected = false;
//Address of the peripheral device. Address will be found during scanning...
static BLEAddress *pServerAddress;
//Characteristicd that we want to read
static BLERemoteCharacteristic* temperatureCharacteristic;
static BLERemoteCharacteristic* humidityCharacteristic;
//Activate notify
const uint8_t notificationOn[] = {0x1, 0x0};
const uint8_t notificationOff[] = {0x0, 0x0};
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
//Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
//Variables to store temperature and humidity
char* temperatureChar;
char* humidityChar;
//Flags to check whether new temperature and humidity readings are available
boolean newTemperature = false;
boolean newHumidity = false;
//Connect to the BLE Server that has the name, Service, and Characteristics
bool connectToServer(BLEAddress pAddress) {
BLEClient* pClient = BLEDevice::createClient();
// Connect to the remove BLE Server.
pClient->connect(pAddress);
Serial.println(" - Connected to server");
// Obtain a reference to the service we are after in the remote BLE server.
BLERemoteService* pRemoteService = pClient->getService(bmeServiceUUID);
if (pRemoteService == nullptr) {
Serial.print("Failed to find our service UUID: ");
Serial.println(bmeServiceUUID.toString().c_str());
return (false);
}
// Obtain a reference to the characteristics in the service of the remote BLE server.
temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureCharacteristicUUID);
humidityCharacteristic = pRemoteService->getCharacteristic(humidityCharacteristicUUID);
if (temperatureCharacteristic == nullptr || humidityCharacteristic == nullptr) {
Serial.print("Failed to find our characteristic UUID");
return false;
}
Serial.println(" - Found our characteristics");
//Assign callback functions for the Characteristics
temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
humidityCharacteristic->registerForNotify(humidityNotifyCallback);
return true;
}
//Callback function that gets called, when another device's advertisement has been received
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
if (advertisedDevice.getName() == bleServerName) { //Check if the name of the advertiser matches
advertisedDevice.getScan()->stop(); //Scan can be stopped, we found what we are looking for
pServerAddress = new BLEAddress(advertisedDevice.getAddress()); //Address of advertiser is the one we need
doConnect = true; //Set indicator, stating that we are ready to connect
Serial.println("Device found. Connecting!");
}
}
};
//When the BLE Server sends a new temperature reading with the notify property
static void temperatureNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {
//store temperature value
temperatureChar = (char*)pData;
newTemperature = true;
}
//When the BLE Server sends a new humidity reading with the notify property
static void humidityNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {
//store humidity value
humidityChar = (char*)pData;
newHumidity = true;
Serial.print(newHumidity);
}
//function that prints the latest sensor readings in the OLED display
void printReadings(){
display.clearDisplay();
// display temperature
display.setTextSize(1);
display.setCursor(0,0);
display.print("Temperature: ");
display.setTextSize(2);
display.setCursor(0,10);
display.print(temperatureChar);
display.setTextSize(1);
display.cp437(true);
display.write(167);
display.setTextSize(2);
Serial.print("Temperature:");
Serial.print(temperatureChar);
#ifdef temperatureCelsius
//Temperature Celsius
display.print("C");
Serial.print("C");
#else
//Temperature Fahrenheit
display.print("F");
Serial.print("F");
#endif
//display humidity
display.setTextSize(1);
display.setCursor(0, 35);
display.print("Humidity: ");
display.setTextSize(2);
display.setCursor(0, 45);
display.print(humidityChar);
display.print("%");
display.display();
Serial.print(" Humidity:");
Serial.print(humidityChar);
Serial.println("%");
}
void setup() {
//OLED display setup
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE,0);
display.setCursor(0,25);
display.print("BLE Client");
display.display();
//Start serial communication
Serial.begin(115200);
Serial.println("Starting Arduino BLE Client application...");
//Init BLE device
BLEDevice::init("");
// Retrieve a Scanner and set the callback we want to use to be informed when we
// have detected a new device. Specify that we want active scanning and start the
// scan to run for 30 seconds.
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->start(30);
}
void loop() {
// If the flag "doConnect" is true then we have scanned for and found the desired
// BLE Server with which we wish to connect. Now we connect to it. Once we are
// connected we set the connected flag to be true.
if (doConnect == true) {
if (connectToServer(*pServerAddress)) {
Serial.println("We are now connected to the BLE Server.");
//Activate the Notify property of each Characteristic
temperatureCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
humidityCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
connected = true;
} else {
Serial.println("We have failed to connect to the server; Restart your device to scan for nearby BLE server again.");
}
doConnect = false;
}
//if new temperature readings are available, print in the OLED
if (newTemperature && newHumidity){
newTemperature = false;
newHumidity = false;
printReadings();
}
delay(1000); // Delay a second between loops.
}
View raw code
Continue reading to learn how the code works or skip to the Demonstration section.
Importing libraries
You start by importing the required libraries:
#include "BLEDevice.h"
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
Choosing temperature unit
By default the client will receive the temperature in Celsius degrees, if you comment the following line or delete it, it will start receiving the temperature in Fahrenheit degrees.
//Default Temperature is in Celsius
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius
BLE Server Name and UUIDs
Then, define the BLE server name that we want to connect to and the service and characteristic UUIDs that we want to read. Leave the default BLE server name and UUIDs to match the ones defined in the server sketch.
//BLE Server name (the other ESP32 name running the server sketch)
#define bleServerName "BME280_ESP32"
/* UUID's of the service, characteristic that we want to read*/
// BLE Service
static BLEUUID bmeServiceUUID("91bad492-b950-4226-aa2b-4ede9fa42f59");
// BLE Characteristics
#ifdef temperatureCelsius
//Temperature Celsius Characteristic
static BLEUUID temperatureCharacteristicUUID("cba1d466-344c-4be3-ab3f-189f80dd7518");
#else
//Temperature Fahrenheit Characteristic
static BLEUUID temperatureCharacteristicUUID("f78ebbff-c8b7-4107-93de-889a6a06d408");
#endif
// Humidity Characteristic
static BLEUUID humidityCharacteristicUUID("ca73b3ba-39f6-4ab3-91ae-186dc9577d99");
Declaring variables
Then, you need to declare some variables that will be used later with Bluetooth to check whether we're connected to the server or not.
//Flags stating if should begin connecting and if the connection is up
static boolean doConnect = false;
static boolean connected = false;
Create a variable of type BLEAddress that refers to the address of the server we want to connect. This address will be found during scanning.
//Address of the peripheral device. Address will be found during scanning...
static BLEAddress *pServerAddress;
Set the characteristics we want to read (temperature and humidity).
//Characteristicd that we want to read
static BLERemoteCharacteristic* temperatureCharacteristic;
static BLERemoteCharacteristic* humidityCharacteristic;
OLED Display
You also need to declare some variables to work with the OLED. Define the OLED width and height:
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
Instantiate the OLED display with the width and height defined earlier.
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire);
Temperature and Humidity Variables
Define char variables to hold the temperature and humidity values received by the server.
//Variables to store temperature and humidity
char* temperatureChar;
char* humidityChar;
The following variables are used to check whether new temperature and humidity readings are available and if it is time to update the OLED display.
//Flags to check whether new temperature and humidity readings are available
boolean newTemperature = false;
boolean newHumidity = false;
printReadings()
We created a function called printReadings() that displays the temperature and humidity readings on the OLED display.
void printReadings(){
display.clearDisplay();
// display temperature
display.setTextSize(1);
display.setCursor(0,0);
display.print("Temperature: ");
display.setTextSize(2);
display.setCursor(0,10);
display.print(temperatureChar);
display.print(" ");
display.setTextSize(1);
display.cp437(true);
display.write(167);
display.setTextSize(2);
Serial.print("Temperature:");
Serial.print(temperatureChar);
#ifdef temperatureCelsius
//Temperature Celsius
display.print("C");
Serial.print("C");
#else
//Temperature Fahrenheit
display.print("F");
Serial.print("F");
#endif
//display humidity
display.setTextSize(1);
display.setCursor(0, 35);
display.print("Humidity: ");
display.setTextSize(2);
display.setCursor(0, 45);
display.print(humidityChar);
display.print("%");
display.display();
Serial.print(" Humidity:");
Serial.print(humidityChar);
Serial.println("%");
}
Recommended reading: ESP32 OLED Display with Arduino IDE
setup()
In the setup(), start the OLED display.
//OLED display setup
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
Then, print a message in the first line saying BME SENSOR.
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE,0);
display.setCursor(0,25);
display.print("BLE Client");
display.display();
Start the serial communication at a baud rate of 115200.
Serial.begin(115200);
And initialize the BLE device.
//Init BLE device
BLEDevice::init("");
Scan nearby devices
The following methods scan for nearby devices.
// Retrieve a Scanner and set the callback we want to use to be informed when we
// have detected a new device. Specify that we want active scanning and start the
// scan to run for 30 seconds.
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->start(30);
MyAdvertisedDeviceCallbacks() function
Note that the MyAdvertisedDeviceCallbacks() function, upon finding a BLE device, checks if the device found has the right BLE server name. If it has, it stops the scan and changes the doConnect boolean variable to true. This way we know that we found the server we're looking for, and we can start establishing a connection.
//Callback function that gets called, when another device's advertisement has been received
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
if (advertisedDevice.getName() == bleServerName) { //Check if the name of the advertiser matches
advertisedDevice.getScan()->stop(); //Scan can be stopped, we found what we are looking for
pServerAddress = new BLEAddress(advertisedDevice.getAddress()); //Address of advertiser is the one we need
doConnect = true; //Set indicator, stating that we are ready to connect
Serial.println("Device found. Connecting!");
}
}
};
Connect to the server
If the doConnect variable is true, it tries to connect to the BLE server. The connectToServer() function handles the connection between the client and the server.
//Connect to the BLE Server that has the name, Service, and Characteristics
bool connectToServer(BLEAddress pAddress) {
BLEClient* pClient = BLEDevice::createClient();
// Connect to the remove BLE Server.
pClient->connect(pAddress);
Serial.println(" - Connected to server");
// Obtain a reference to the service we are after in the remote BLE server.
BLERemoteService* pRemoteService = pClient->getService(bmeServiceUUID);
if (pRemoteService == nullptr) {
Serial.print("Failed to find our service UUID: ");
Serial.println(bmeServiceUUID.toString().c_str());
return (false);
}
// Obtain a reference to the characteristics in the service of the remote BLE server.
temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureCharacteristicUUID);
humidityCharacteristic = pRemoteService->getCharacteristic(humidityCharacteristicUUID);
if (temperatureCharacteristic == nullptr || humidityCharacteristic == nullptr) {
Serial.print("Failed to find our characteristic UUID");
return false;
}
Serial.println(" - Found our characteristics");
//Assign callback functions for the Characteristics
temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
humidityCharacteristic->registerForNotify(humidityNotifyCallback);
return true;
}
It also assigns a callback function responsible to handle what happens when a new value is received.
//Assign callback functions for the Characteristics
temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
humidityCharacteristic->registerForNotify(humidityNotifyCallback);
After the BLE client is connected to the server, you need to active the notify property for each characteristic. For that, use the writeValue() method on the descriptor.
if (connectToServer(*pServerAddress)) {
Serial.println("We are now connected to the BLE Server.");
//Activate the Notify property of each Characteristic
temperatureCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
humidityCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
Notify new values
When the client receives a new notify value, it will call these two functions: temperatureNotifyCallback() and humidityNotifyCallback() that are responsible for retrieving the new value, update the OLED with the new readings and print them on the Serial Monitor.
//When the BLE Server sends a new temperature reading with the notify property
static void temperatureNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {
//store temperature value
temperatureChar = (char*)pData;
newTemperature = true;
}
//When the BLE Server sends a new humidity reading with the notify property
static void humidityNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {
//store humidity value
humidityChar = (char*)pData;
newHumidity = true;
Serial.print(newHumidity);
}
These two previous functions are executed every time the BLE server notifies the client with a new value, which happens every 30 seconds. These functions save the values received on the temperatureChar and humidityChar variables. These also change the newTemperature and newHumidity variables to true, so that we know we've received new readings.
Display new temperature and humidity readings
In the loop(), there is an if statement that checks if new readings are available. If there are new readings, we se the newTemperature and newHumidity variables to false, so that we are able to receive new readings later on. Then, we call the printReadings() function to display the readings on the OLED.
//if new temperature readings are available, print in the OLED
if (newTemperature && newHumidity){
newTemperature = false;
newHumidity = false;
printReadings();
}
Testing the Project
That's it for the code. You can upload it to your ESP32 board.
Once the code is uploaded. Power the ESP32 BLE server, then power the ESP32 with the client sketch. The client starts scanning nearby devices, and when it finds the other ESP32, it establishes a Bluetooth connection. Every 30 seconds, it updates the display with the latest readings.
Important: don't forget to disconnect your smartphone from the BLE server. Otherwise, the ESP32 BLE Client won't be able to connect to the server.
Wrapping Up
In this tutorial, you learned how to create a BLE Server and a BLE Client with the ESP32. You learned how to set new temperature and humidity values on the BLE server characteristics. Then, other BLE devices (clients) can connect to that server and read those characteristic values to get the latest temperature and humidity values. Those characteristics have the notify property, so that the client is notified whenever there's a new value.
Using BLE is another communication protocol you can use with the ESP32 boards besides Wi-Fi. We hope you found this tutorial useful. We have tutorials for other communication protocols that you may find useful.
ESP32 Bluetooth Classic with Arduino IDE Getting Started
ESP32 Useful Wi-Fi Library Functions (Arduino IDE)
ESP-MESH with ESP32 and ESP8266: Getting Started (painlessMesh library)
Getting Started with ESP-NOW (ESP32 with Arduino IDE)
Firebase Authentication (Email and Password)
In this guide, you'll learn how to authenticate to Firebase using your ESP32 or ESP8266 board with an email and password. This is useful for restricting or allowing access to certain users or creating multiple users that can access the same Firebase project. Additionally, this is helpful to set up database rules to protect your project's data.
Other Firebase Tutorials with the ESP32 that you might be interested in:
ESP32: Getting Started with Firebase (Realtime Database)
ESP32 with Firebase Creating a Web App
What is Firebase?
Firebase is Google's mobile application development platform that helps you build, improve, and grow your app. It has many services used to manage data from any android, IOS, or web application like authentication, realtime database, hosting, etc. In this tutorial, we'll focus on the authentication part.
Project Overview
Create Firebase Project
Set Authentication Methods
Get Project API Key
Authentication with ESP32/ESP8266
1) Create Firebase Proje3t
1) Go to Firebase and sign in using a Google Account.
2) Click Get Started and then Add project to create a new project.
3) Give a name to your project, for example: ESP Firebase Demo.
4) Disable the option Enable Google Analytics for this project as it is not needed and click Create project.
5) It will take a few seconds to set up your project. Then, click Continue when it's ready.
6) You'll be redirected to your Project console page.
2) Set Authentication Methods
To allow authentication with email and password, first, you need to set authentication methods for your app.
Most apps need to know the identity of a user. In other words, it takes care of logging in and identifying the users (in this case, the ESP32 or ESP8266). Knowing a user's identity allows an app to securely save user data in the cloud and provide the same personalized experience across all of the user's devices. To learn more about the authentication methods, you can read the documentation.
1) On the left sidebar, click on Authentication and then on Get started.
2) Select the Option Email/Password.
3) Enable that authentication method and click Save.
4) The authentication with email and password should now be enabled.
5) Now, you need to add a user. Still on the Authentication tab, select the Users tab at the top. Then, click on Add User.
6) Add an email address for the authorized user. It can be your google account email or any other email. You can also create an email for this specific project. Add a password that will allow you to sign in to your app and access the database. Don't forget to save the password in a safe place because you'll need it later. When you're done, click Add user.
7) A new user was successfully created and added to the Users table.
Notice that Firebase creates a unique UID for each registered user. The user UID allows us to identify the user and keep track of the user to provide or deny access to the project or the database. There's also a column that registers the date of the last sign-in. At the moment, it is empty because we haven't signed in with that user yet.
3) Get Project API Key
To interface with your Firebase project using the ESP32 or ESP8266 boards, you need to get your project API key. Follow the next steps to get your project API key.
1) On the left sidebar, click on Project Settings.
2) Copy the Web API Key to a safe place because you'll need it later.
4) Authentication with ESP32/ESP8266
Now that your Firebase Project is created and you've set up the authentication method, you'll learn to log in with the ESP32 using the authorized user email and password.
To program the ESP32, you can use Arduino IDE, VS Code with the PlatformIO extension, or other suitable software.
Note: for firebase projects, we recommend using VS Code with the PlatformIO extension because if you want to develop a web application to make the bridge between the ESP32 and Firebase, VS Code provides all the tools to do that. However, we won't build the web application in this tutorial, so you can use Arduino IDE.
Install the Firebase-ESP-Client Library
There is a library with lots of examples to use Firebase with the ESP32: the Firebase-ESP-Client library. This library is compatible with both the ESP32 and ESP8266 boards.
If you like this library and you'll use it in your projects, consider supporting the developer's work.
In this tutorial, we'll look at a simple example to authenticate the ESP32. The library provides many other examples that you can check here. It also offers detailed documentation explaining how to use the library.
Installation VS Code + PlatformIO
If you're using VS Code with the PlatformIO extension, click on the PIO Home icon and select the Libraries tab. Search for Firebase ESP Client. Select the Firebase Arduino Client Library for ESP8266 and ESP32.
Then, click Add to Project and select the project you're working on.
Also, change the monitor speed to 115200 by adding the following line to the platformio.ini file of your project:
monitor_speed = 115200
Installation Arduino IDE
If you're using Arduino IDE, follow the next steps to install the library.
Go to Sketch > Include Library > Manage Libraries
Search for Firebase ESP Client and install the Firebase Arduino Client Library for ESP8266 and ESP32 by Mobitz.
Now, you're all set to start programming the ESP32 or ESP8266 board to interact with the database.
ESP32/ESP8266 Firebase Authentication Email/Password
Copy the following code to the Arduino IDE or to the main.cpp file if you're using VS Code.
/*
Rui Santos
Complete project details at our blog: https://RandomNerdTutorials.com/esp32-esp8266-firebase-authentication/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Based in the Authenticatiions Examples by Firebase-ESP-Client library by mobizt: https://github.com/mobizt/Firebase-ESP-Client/tree/main/examples/Authentications
*/
#include <Arduino.h>
#if defined(ESP32)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#endif
#include <Firebase_ESP_Client.h>
// Provide the token generation process info.
#include "addons/TokenHelper.h"
// Provide the RTDB payload printing info and other helper functions.
#include "addons/RTDBHelper.h"
// Insert your network credentials
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// Insert Firebase project API Key
#define API_KEY "REPLACE_WITH_YOUR_FIREBASE_PROJECT_API_KEY"
// Insert Authorized Email and Corresponding Password
#define USER_EMAIL "[email protected]"
#define USER_PASSWORD "your_user_password"
// Define Firebase objects
FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
// Variable to save USER UID
String uid;
// Initialize WiFi
void initWiFi() {
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
Serial.println();
}
void setup(){
Serial.begin(115200);
// Initialize WiFi
initWiFi();
// Assign the api key (required)
config.api_key = API_KEY;
// Assign the user sign in credentials
auth.user.email = USER_EMAIL;
auth.user.password = USER_PASSWORD;
Firebase.reconnectWiFi(true);
fbdo.setResponseSize(4096);
// Assign the callback function for the long running token generation task
config.token_status_callback = tokenStatusCallback; //see addons/TokenHelper.h
// Assign the maximum retry of token generation
config.max_token_generation_retry = 5;
// Initialize the library with the Firebase authen and config
Firebase.begin(&config, &auth);
// Getting the user UID might take a few seconds
Serial.println("Getting User UID");
while ((auth.token.uid) == "") {
Serial.print('.');
delay(1000);
}
// Print user UID
uid = auth.token.uid.c_str();
Serial.print("User UID: ");
Serial.print(uid);
}
void loop(){
if (Firebase.isTokenExpired()){
Firebase.refreshToken(&config);
Serial.println("Refresh token");
}
}
View raw code
You need to insert your network credentials, URL database, and project API key for the project to work.
This sketch was based on the authentication basic example provided by the library. You can find more examples here.
How the Code Works
Continue reading to learn how the code works, or skip to the demonstration section.
Include Libraries
First, include the required libraries. The WiFi.h library to connect the ESP32 to the internet (or the ESP8266WiFi.h library for the ESP8266 board) and the Firebase_ESP_Client.h library to interface the boards with Firebase.
#include <Arduino.h>
#if defined(ESP32)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#endif
#include <Firebase_ESP_Client.h>
You also need to include the following for the Firebase library to work.
// Provide the token generation process info.
#include "addons/TokenHelper.h"
// Provide the RTDB payload printing info and other helper functions.
#include "addons/RTDBHelper.h"
Network Credentials
Include your network credentials in the following lines so that your boards can connect to the internet using your local network.
// Insert your network credentials
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
Firebase Project API Key and Firebase User
Insert your Firebase project API keythe one you've gotten in this section.
#define API_KEY "REPLACE_WITH_YOUR_PROJECT_API_KEY"
Insert the authorized email and the corresponding passwordthese are the details of the user you've added in this section.
// Insert Authorized Email and Corresponding Password
#define USER_EMAIL "REPLACE_WITH_THE_USER_EMAIL"
#define USER_PASSWORD "REPLACE_WITH_THE_USER_PASSWORD"
Firebase Objects and Other Variables
The following line defines a FirebaseData object.
FirebaseData fbdo;
The next line defines a FirebaseAuth object needed for authentication.
FirebaseAuth auth;
Finally, the following line defines FirebaseConfig object needed for configuration data.
FirebaseConfig config;
The uid variable will be used to save the user's UID. We can get the user's UID after the authentication.
String uid;
initWiFi()
The initWiFi() function connects your ESP to the internet using the network credentials provided. You must call this function later in the setup() to initialize WiFi.
// Initialize WiFi
void initWiFi() {
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
Serial.println();
}
setup()
In setup(), initialize the Serial Monitor for debugging purposes at a baud rate of 115200.
Serial.begin(115200);
Call the initWiFi() function to initialize WiFi.
initWiFi();
Assign the API key to the Firebase configuration.
config.api_key = API_KEY;
The following lines assign the email and password to the Firebase authentication object.
auth.user.email = USER_EMAIL;
auth.user.password = USER_PASSWORD;
Add the following to the configuration object.
// Assign the callback function for the long running token generation task
config.token_status_callback = tokenStatusCallback; //see addons/TokenHelper.h
// Assign the maximum retry of token generation
config.max_token_generation_retry = 5;
Finally, initialize the Firebase library (authenticate) with the configuration and authentication settings we defined earlier.
// Initialize the library with the Firebase authen and config
Firebase.begin(&config, &auth);
After initializing the library, we can get the user UID by calling auth.token.uid. Getting the user's UID might take some time, so we add a while loop that waits until we get it.
// Getting the user UID might take a few seconds
Serial.println("Getting User UID");
while ((auth.token.uid) == "") {
Serial.print('.');
delay(1000);
}
Finally, we save the user's UID in the uid variable and print it in the Serial Monitor.
uid = auth.token.uid.c_str();
Serial.print("User UID: ");
Door Status Monitor with Telegram Notifications
In this project, you're going to monitor the status of a door using an ESP32 board and a magnetic reed switch. You'll receive a message in your Telegram account whenever the door changes state: opened or closed. As long as you have access to the internet on your smartphone, you'll be notified no matter where you are. The ESP32 board will be programmed using Arduino IDE.
We have a similar tutorial that sends emails instead of Telegram messages:
ESP32 Door Status Monitor with Email Notifications (IFTTT)
Read the ESP8266 Guide: Door Status Monitor with Telegram Notifications
Project Overview
In this project, we'll create a Telegram Bot that will send messages to your Telegram account whenever a door changes state. To detect the change, we'll use a magnetic contact switch.
A magnetic contact switch is basically a reed switch encased in a plastic shell so that you can easily apply it to a door, a window, or a drawer to detect if it is open or closed.
The electrical circuit is closed when a magnet is near the switchdoor closed. When the magnet is far away from the switchdoor openthe circuit is open. See the figure below.
We can connect the reed switch to an ESP32 GPIO to detect changes in its state.
Introducing Telegram
Telegram Messenger is a cloud-based instant messaging and voice over IP service. You can easily install it on your smartphone (Android and iPhone) or computer (PC, Mac, and Linux). It is free and without any ads. Telegram allows you to create bots that you can interact with.
Bots are third-party applications that run inside Telegram. Users can interact with bots by sending them messages, commands, and inline requests. You control your bots using HTTPS requests to Telegram Bot API.
The ESP32 will interact with the Telegram bot to send messages to your Telegram account. Whenever the door changes state, you'll receive a notification on your smartphone (as long as you have access to the internet).
Creating a Telegram Bot
Go to Google Play or App Store, download, and install Telegram.
Open Telegram and follow the next steps to create a Telegram Bot. First, search for botfather and click the BotFather as shown below. Or open this link t.me/botfather on your smartphone.
The following window should open, and you'll be prompted to click the start button.
Type /newbot and follow the instructions to create your bot. Give it a name and username. Mine is called Door Sensor, and the username is ESPDoorSensorBot.
If your bot is successfully created, you'll receive a message with a link to access the bot and the bot token. Save the bot token because you'll need it so that the ESP32 can interact with the bot.
Sending a Message to the Bot
This step is very important. Don't miss it. Otherwise, the project will not work.
You must send a message to your Telegram Bot from your Telegram account before it can send you messages.
1) Go back to the chats tab, and in the search field, type the username of your bot.
2) Select your bot to start a conversation.
3) Click on the Start link.
And that's it! You can proceed to the next section.
Get Your Telegram User ID
To send a message to your Telegram account, the bot needs to know your user ID.
In your Telegram account, search for myidbot or open this link t.me/myidbot on your smartphone.
Start a conversation with that bot and type /getid. You will get a reply back with your user ID. Save that user ID, because you'll need it later in this tutorial.
Preparing Arduino IDE
We'll program the ESP32 board using Arduino IDE, so make sure you have it installed in your Arduino IDE.
Installing the ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
Universal Telegram Bot Library
To interact with the Telegram bot, we'll use the Universal Telegram Bot Library created by Brian Lough that provides an easy interface for the Telegram Bot API.
Follow the next steps to install the latest release of the library.
Click here to download the Universal Arduino Telegram Bot library.
Go to Sketch > Include Library > Add.ZIP Library...
Add the library you've just downloaded.
Important: don't install the library through the Arduino Library Manager because it might install a deprecated version.
For all the details about the library, take a look at the Universal Arduino Telegram Bot Library GitHub page.
ArduinoJson Library
You also have to install the ArduinoJson library. Follow the next steps to install the library.
Go to Skech > Include Library > Manage Libraries.
Search for ArduinoJson.
Install the library.
We're using ArduinoJson library version 6.5.12.
Parts Required
Here's the hardware that you need to complete this project:
ESP32 read Best ESP32 Development Boards
1 Magnetic Reed Switch
1 10k resistor
1 breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic ESP32 with Reed Switch
We wired the reed switch to GPIO 4, but you can connect it to any suitable GPIO.
Code
Copy the sketch below to your Arduino IDE. Replace the SSID, password, BOT token, and user ID with your credentials.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-door-status-telegram/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
// Set GPIOs for LED and reedswitch
const int reedSwitch = 4;
const int led = 2; //optional
// Detects whenever the door changed state
bool changeState = false;
// Holds reedswitch state (1=opened, 0=close)
bool state;
String doorState;
// Auxiliary variables (it will only detect changes that are 1500 milliseconds apart)
unsigned long previousMillis = 0;
const long interval = 1500;
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Initialize Telegram BOT
#define BOTtoken "XXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" // your Bot Token (Get from Botfather)
// Use @myidbot to find out the chat ID of an individual or a group
// Also note that you need to click "start" on a bot before it can
// message you
#define CHAT_ID "XXXXXXXXXX"
WiFiClientSecure client;
UniversalTelegramBot bot(BOTtoken, client);
// Runs whenever the reedswitch changes state
ICACHE_RAM_ATTR void changeDoorStatus() {
Serial.println("State changed");
changeState = true;
}
void setup() {
// Serial port for debugging purposes
Serial.begin(115200);
// Read the current door state
pinMode(reedSwitch, INPUT_PULLUP);
state = digitalRead(reedSwitch);
// Set LED state to match door state
pinMode(led, OUTPUT);
digitalWrite(led, !state);
// Set the reedswitch pin as interrupt, assign interrupt function and set CHANGE mode
attachInterrupt(digitalPinToInterrupt(reedSwitch), changeDoorStatus, CHANGE);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
client.setCACert(TELEGRAM_CERTIFICATE_ROOT); // Add root certificate for api.telegram.org
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
bot.sendMessage(CHAT_ID, "Bot started up", "");
}
void loop() {
if (changeState){
unsigned long currentMillis = millis();
if(currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
// If a state has occured, invert the current door state
state = !state;
if(state) {
doorState = "closed";
}
else{
doorState = "open";
}
digitalWrite(led, !state);
changeState = false;
Serial.println(state);
Serial.println(doorState);
//Send notification
bot.sendMessage(CHAT_ID, "The door is " + doorState, "");
}
}
}
View raw code
How the Code Works
Continue reading to learn how the code works, or proceed to the Demonstration section.
First, include the required libraries.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
Set the GPIOs for the reed switch and LED (the on-board LED is GPIO 2). We'll light up the on-board LED when the door is open.
const int reedSwitch = 4;
const int led = 2; //optional
The changeState boolean variable indicates whether the door has changed state.
bool changeState = false;
The state variable will hold the reed switch state, and the doorState, as the name suggests, will hold the door stateclosed or opened.
bool state;
String doorState;
The following timer variables allow us to debounce the switch. Only changes that have occurred with at least 1500 milliseconds between them will be considered.
unsigned long previousMillis = 0;
const long interval = 1500;
Insert your SSID and password in the following variables so that the ESP32 can connect to the internet.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Insert your Telegram Bot Tokenthe one you've gotten in this step.
#define BOTtoken "XXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Insert your chat IDthe one you've gotten in this step.
#define CHAT_ID "XXXXXXXXXX"
Create a new Wi-Fi client with WiFiClientSecure.
WiFiClientSecure client;
Create a bot with the token and client defined earlier.
UniversalTelegramBot bot(BOTtoken, client);
The changeDoorStatus() function will run whenever a change is detected on the door state. This function simply changes the changeState variable to true. Then, in the loop(), we'll handle what happens when the state changes (invert the previous door state and send a message to your Telegram account).
ICACHE_RAM_ATTR void changeDoorStatus() {
Serial.println("State changed");
changeState = true;
}
setup()
In the setup(), initialize the Serial Monitor for debugging purposes:
Serial.begin(115200);
Set the reed switch as an INPUT. And save the current state when the ESP32 first starts.
pinMode(reedSwitch, INPUT_PULLUP);
state = digitalRead(reedSwitch);
Set the LED as an OUTPUT and set its state to match the reed switch state (circuit closed and LED off; circuit opened and LED on).
pinMode(led, OUTPUT);
digitalWrite(led, !state);
Setting an interrupt
Set the reed switch as an interrupt.
attachInterrupt(digitalPinToInterrupt(reedSwitch), changeDoorStatus, CHANGE);
To set an interrupt in the Arduino IDE, you use the attachInterrupt() function, which accepts as arguments: the GPIO interrupt pin, the name of the function to be executed, and mode.
The first argument is a GPIO interrupt. You should use digitalPinToInterrupt(GPIO) to set the actual GPIO as an interrupt pin.
The second argument of the attachInterrupt() function is the name of the function that will be called every time the interrupt is triggered the interrupt service routine (ISR). In this case, it is the changeDoorStatus function.
The ISR function should be as simple as possible, so the processor gets back to the execution of the main program quickly.
The third argument is the mode. We set it to CHANGE to trigger the interrupt whenever the pin changes value for example, from HIGH to LOW and LOW to HIGH.
To learn more about interrupts with the ESP32, read the following tutorial:
ESP32 Interrupts and Timers using Arduino IDE
Initialize Wi-Fi
The following lines connect the ESP32 to Wi-Fi and add a root certificate for api.telegram.org.
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
client.setCACert(TELEGRAM_CERTIFICATE_ROOT); // Add root certificate for api.telegram.org
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Send a message to your Telegram account informing you that the bot started.
bot.sendMessage(CHAT_ID, "Bot started up", "");
loop()
In the loop(), we'll read the changeState variable, and if a change has occurred, we'll send a message to your Telegram account.
First, check if a change occurred:
if (changeState){
Then, check if at least 1500 milliseconds have passed since the last state change.
if(currentMillis - previousMillis >= interval) {
If that's true, reset the timer and invert the current switch state:
state = !state;
If the reed switch state is 1(true), the door is closed. So, we change the doorState variable to closed.
if(state) {
doorState = "closed";
}
If it's 0(false), the door is opened.
else{
doorState = "open";
}
Set the LED state accordingly and print the door state in the Serial Monitor.
digitalWrite(led, !state);
changeState = false;
Serial.println(state);
Serial.println(doorState);
Finally, the following line sends a notification to your Telegram account with the current door state.
bot.sendMessage(CHAT_ID, "The door is " + doorState, "");
Demonstration
After modifying the sketch to include your network credentials, bot token, and user ID, upload it to your ESP32. Go to Tools > Board and select your ESP32 board. Then, go to Tools > Port and select the COM port the ESP32 is connected to.
Open the Serial Monitor at a baud rate of 115200 to check if the changes are detected.
For prototyping/testing, you can apply the magnetic reed switch to your door using Velcro.
Now when someone opens/closes your door, you receive a message in your Telegram account.
Wrapping Up
In this tutorial, you've learned how to send notifications to your Telegram account when the reed switch changes state. This can be useful to detect if a door, window, or drawer was opened or closed.
Web Server: Control Stepper Motor (WebSocket)
In this guide you'll learn how to create a web server with the ESP32 that displays a web page to control a stepper motor. The web page allows you to insert the number of steps and select clockwise or counterclockwise direction. Additionally, it also shows whether the motor is currently spinning or if it is stopped. The communication between the client and the server is achieved via WebSocket protocol. All clients are updated with the current motor state.
This tutorial is the second part of this article ESP32 Web Server: Control Stepper Motor (HTML Form), but it can also be followed as a standalone tutorial.
To better understand how this project works, you can take a look at the following tutorials:
ESP32 with Stepper Motor (28BYJ-48 and ULN2003 Motor Driver)
ESP32 Web Server: Control Stepper Motor (HTML Form)
ESP32 WebSocket Server: Control Outputs (Arduino IDE)
Table of Contents
Prerequisites
Project Overview
Organizing your files:
HTML File
CSS File
JavaScript File
Arduino Sketch
Upload Code and Files
Demonstration
Prerequisites
Before proceeding with the tutorial, make sure you check the following prerequisites.
1) Parts Required
To follow this tutorial, you need the following parts:
28BYJ-48 Stepper Motor + ULN2003 Motor Driver
ESP32 (read Best ESP32 Development Boards)
Jumper Wires
5V Power Supply
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
2) Arduino IDE and ESP32 Boards Add-on
We'll program the ESP32 using Arduino IDE. So, you must have the ESP32 add-on installed. Follow the next tutorial if you haven't already:
Installing ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP32:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
3) Filesystem Uploader Plugin
To upload the HTML, CSS, and JavaScript files needed to build this project to the ESP32 filesystem (SPIFFS), we'll use a plugin for Arduino IDE: SPIFFS Filesystem uploader. Follow the next tutorial to install the filesystem uploader plugin if you haven't already:
ESP32: Install SPIFFS FileSystem Uploader Plugin in Arduino IDE
If you're using VS Code with the PlatformIO extension, read the following tutorial to learn how to upload files to the filesystem:
ESP32 with VS Code and PlatformIO: Upload Files to Filesystem (SPIFFS)
4) Libraries
To build this project, you need to install the following libraries:
ESPAsyncWebServer (.zip folder)
AsyncTCP (.zip folder)
The ESPAsyncWebServer and AsynTCP libraries aren't available to install through the Arduino Library Manager. You need to click on the previous links to download the library files. Then, in your Arduino IDE, go to Sketch> Include Library > Add .zip Library and select the libraries you've just downloaded.
Installing Libraries (VS Code + PlatformIO)
If you're programming the ESP32 using PlatformIO, you should add the following lines to the platformio.ini file to include the libraries (also change the Serial Monitor speed to 115200):
monitor_speed=115200
lib_deps = ESP Async WebServer
arduino-libraries/Stepper @ ^1.1.3
5) Schematic Diagram
The following schematic diagram shows the connections between the stepper motor and the ESP32.
Note: You should power the ULN2003APG motor driver using an external 5V power supply.
Motor Driver |
ESP32 |
IN1 |
GPIO 19 |
IN2 |
GPIO 18 |
IN3 |
GPIO 5 |
IN4 |
GPIO 17 |
Project Overview
The following video shows a quick demonstration of what you'll achieve by the end of this tutorial.
The following image shows the web page you'll build for this project.
The web page shows a form where you can enter the number of steps you want the motor to move and select the direction: clockwise or counterclockwise.
It also shows the motor state: motor spinning or motor stopped. Additionally, there's a gear icon that spins as long as the motor is spinning. The gear spins clockwise or counterclockwise accordingly to the chosen direction.
The server and the client communicate using WebSocket protocol.
When you click on the GO! button, it calls a Javascript function that sends a message via WebSocket protocol with all the information: steps and direction (3). The message is in the following format:
steps&direction
So, if you submit 2000 steps and clockwise direction, it will send the following message:
2000&CW
At the same time, it will change the motor state on the web page, and the gear will start spinning in the proper direction (2).
Then, the server receives the message (4) and spins the motor accordingly (5).
When the motor stops spinning (6), the ESP will send a message to the client(s), also via WebSocket protocol, informing that the motor has stopped (7).
The client(s) receive this message and update the motor state on the web page (8).
Organizing your Files
The files you want to upload to the ESP filesystem should be placed in a folder called data under the project folder. We'll move three files to that folder:
index.html to build the web page;
style.css to style the web page;
script.js to handle websocket communication and start/stop the gear animation.
You should save the HTML, CSS, and JavaScript files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We'll upload those files to the ESP32 filesystem (SPIFFS).
You can download all project files:
Download All the Arduino Project Files
HTML File
Create a file called index.html with the following content:
<!DOCTYPE html>
<html>
<head>
<title>Stepper Motor</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
</head>
<body>
<div>
<h1>Stepper Motor Control <i></i></h2>
</div>
<div>
<form>
<input type="radio" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
<label for="steps">Number of steps:</label>
<input type="number" name="steps">
</form>
<button onclick="submitForm()">GO!</button>
<p>Motor state: <span>Stopped</span></p>
<p><i></i> </p>
</div>
</body>
<script src="script.js"></script>
</html>
View raw code
This HTML file is very similar to the one used in the previous tutorial. You can click here for a complete explanation of the HTML file.
We've added ids to the HTML elements we want to manipulate using JavaScriptthe radio buttons and the input field:
clockwise radio button: id=CW
counterclowise radio button: id=CCW
steps input field: id=steps
<input type="radio" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
<label for="steps">Number of steps:</label>
<input type="number" name="steps">
We want to send the form results to the server (ESP32) via WebSocket protocol. So, we've added a button, that when clicked (onclick event) calls the submitForm() user-defined javascript function that sends the results to the server as you'll see later in the JavaScript section.
<button onclick="submitForm()">GO!</button>
Additionally, we also added a paragraph to display the motor state. We've added a <span> tag with the motor-state id so that we're able to manipulate the text between the <span> tags using Javascript.
<p>Motor state: <span>Stopped</span></p>
Finally, there's a paragraph displaying a gear with the id=gear. We need this id to make the gear move.
<p><i></i> </p>
Don't forget that you need to reference the JavaScript file (scrip.js) in the HTML file as follows:
<script src="script.js"></script>
CSS File
Create a file called style.css with the following content:
html {
font-family: Arial, Helvetica, sans-serif;
}
h1 {
font-size: 1.8rem;
color: white;
}
p{
font-size: 20px;
text-align: center;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
text-align: center;
}
body {
margin: 0;
}
.content {
padding: 20px;
max-width: max-content;
margin: 0 auto;
}
input[type=number], select {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
form{
border-radius: 5px;
background-color: #f2f2f2;
padding: 20px;
}
button {
background-color: #034078;
border: none;
padding: 14px 20px;
text-align: center;
font-size: 20px;
border-radius: 4px;
transition-duration: 0.4s;
width: 100%;
color: white;
cursor: pointer;
}
button:hover {
background-color: #1282A2;
}
input[type="radio"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 50%;
width: 16px;
height: 16px;
border: 2px solid #999;
transition: 0.2s all linear;
margin-right: 5px;
position: relative;
top: 4px;
}
input[type="radio"]:checked{
border: 6px solid #1282A2;
}
#motor-state{
font-weight: bold;
color: red;
}
#gear{
font-size:100px;
color:#2d3031cb;
}
.spin {
-webkit-animation:spin 4s linear infinite;
-moz-animation:spin 4s linear infinite;
animation:spin 4s linear infinite;
}
.spin-back {
-webkit-animation:spin-back 4s linear infinite;
-moz-animation:spin-back 4s linear infinite;
animation:spin-back 4s linear infinite;
}
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
@-moz-keyframes spin-back { 100% { -moz-transform: rotate(-360deg); } }
@-webkit-keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); } }
@keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); transform:rotate(-360deg); } }
View raw code
We already covered how the CSS for the HTML form works. You can click here for a detailed explanation. Let's take a look at the relevant parts for this tutorial.
We format the motor state text font-weight (bold) and color (red). To refer to a specific id in CSS, use # followed by the id (#motor-state).
#motor-state{
font-weight: bold;
color: red;
}
The following line formats the gear icon color and sizeremember that its id is gear, so we refer to it with #gear:
#gear{
font-size:100px;
color:#2d3031cb;
}
Then, we format two classes spin and spin-back that are not attributed to any HTML element yet. We'll attribute the spin and spin-back classes to the gear using JavaScript when the motor starts moving.
These classes use the animation property to rotate the gear. To learn more about how the animation property works, we recommend taking a look at this quick tutorial.
.spin {
-webkit-animation:spin 4s linear infinite;
-moz-animation:spin 4s linear infinite;
animation:spin 4s linear infinite;
}
.spin-back {
-webkit-animation:spin-back 4s linear infinite;
-moz-animation:spin-back 4s linear infinite;
animation:spin-back 4s linear infinite;
}
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
@-moz-keyframes spin-back { 100% { -moz-transform: rotate(-360deg); } }
@-webkit-keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); } }
@keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); transform:rotate(-360deg); } }
JavaScript File
Create a file called script.js with the following content:
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onload);
var direction;
function onload(event) {
initWebSocket();
}
function initWebSocket() {
console.log('Trying to open a WebSocket connection');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
function onOpen(event) {
console.log('Connection opened');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function submitForm(){
const rbs = document.querySelectorAll('input[name="direction"]');
direction;
for (const rb of rbs) {
if (rb.checked) {
direction = rb.value;
break;
}
}
document.getElementById("motor-state").innerHTML = "motor spinning...";
document.getElementById("motor-state").style.color = "blue";
if (direction=="CW"){
document.getElementById("gear").classList.add("spin");
}
else{
document.getElementById("gear").classList.add("spin-back");
}
var steps = document.getElementById("steps").value;
websocket.send(steps+"&"+direction);
}
function onMessage(event) {
console.log(event.data);
direction = event.data;
if (direction=="stop"){
document.getElementById("motor-state").innerHTML = "motor stopped"
document.getElementById("motor-state").style.color = "red";
document.getElementById("gear").classList.remove("spin", "spin-back");
}
else if(direction=="CW" || direction=="CCW"){
document.getElementById("motor-state").innerHTML = "motor spinning...";
document.getElementById("motor-state").style.color = "blue";
if (direction=="CW"){
document.getElementById("gear").classList.add("spin");
}
else{
document.getElementById("gear").classList.add("spin-back");
}
}
}
View raw code
Let's see how the JavaScript for this project works.
The gateway is the entry point to the WebSocket interface. window.location.hostname gets the current page address (the web server IP address)
var gateway = `ws://${window.location.hostname}/ws`;
Create a new global variable called websocket.
var websocket;
Create another global variable called direction that will hold the motor's current direction: clockwise, counterclowise or stopped.
var direction;
Add an event listener that will call the onload function when the web page loads.
window.addEventListener('load', onload);
The onload() function calls the initWebSocket() function to initialize a WebSocket connection with the server.
function onload(event) {
initWebSocket();
}
The initWebSocket() function initializes a WebSocket connection on the gateway defined earlier. We also assign several callback functions that will be triggered when the WebSocket connection is opened, closed or when a message is received.
function initWebSocket() {
console.log('Trying to open a WebSocket connection');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
When the connection is opened, print a message in the console for debugging purposes.
function onOpen(event) {
console.log('Connection opened');
}
If for some reason the web socket connection is closed, call the initWebSocket() function again after 2000 milliseconds (2 seconds).
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
Finally, we need to handle what happens when the form is submitted and when the client receives a new message (onMessage event).
When the form is submitted, the submitForm() function is called:
function submitForm(){
We start by getting which radio button is selected. We save the value of the selected radio button in the direction variable.
const rbs = document.querySelectorAll('input[name="direction"]');
var direction;
for (const rb of rbs) {
if (rb.checked) {
direction = rb.value;
break;
}
}
Then, we change the motor state text to motor spinning and its color to blue. We refer to that HTML element by its id motor-state.
document.getElementById("motor-state").innerHTML = "motor spinning...";
document.getElementById("motor-state").style.color = "blue";
Then, we check whether we've selected clockwise or counterclockwise direction to spin the gear in the right direction. To do that, we add the class spin or spin-back to the element with the gear id.
if (direction=="CW"){
document.getElementById("gear").classList.add("spin");
}
else{
document.getElementById("gear").classList.add("spin-back");
}
We get the number of steps inserted and save it in the steps variable.
var steps = document.getElementById("steps").value;
Then, we finally send a message via WebSocket protocol to the server (ESP32) with the number of steps and direction separated by a &.
websocket.send(steps+"&"+direction);
The server (your ESP board) will send a message when it is time to change the motor state. When that happens, we save the message in the direction variable.
We check the content of the message and change the motor state and gear animation accordingly.
function onMessage(event) {
console.log(event.data);
direction = event.data;
if (direction=="stop"){
document.getElementById("motor-state").innerHTML = "motor stopped"
document.getElementById("motor-state").style.color = "red";
document.getElementById("gear").classList.remove("spin", "spin-back");
}
else if(direction=="CW" || direction=="CCW"){
document.getElementById("motor-state").innerHTML = "motor spinning...";
document.getElementById("motor-state").style.color = "blue";
if (direction=="CW"){
document.getElementById("gear").classList.add("spin");
}
else{
document.getElementById("gear").classList.add("spin-back");
}
}
}
Arduino Sketch
Before uploading, you can use the following link to:
Download All the Arduino Project Files
Copy the following code to the Arduino IDE. Insert your network credentials and it will work straight away.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/stepper-motor-esp32-websocket/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Stepper.h>
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
#define IN1 19
#define IN2 18
#define IN3 5
#define IN4 17
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
String message = "";
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
// Create a WebSocket object
AsyncWebSocket ws("/ws");
//Variables to save values from HTML form
String direction ="STOP";
String steps;
bool newRequest = false;
// Initialize SPIFFS
void initSPIFFS() {
if (!SPIFFS.begin(true)) {
Serial.println("An error has occurred while mounting SPIFFS");
}
else{
Serial.println("SPIFFS mounted successfully");
}
}
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void notifyClients(String state) {
ws.textAll(state);
}
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
message = (char*)data;
steps = message.substring(0, message.indexOf("&"));
direction = message.substring(message.indexOf("&")+1, message.length());
Serial.print("steps");
Serial.println(steps);
Serial.print("direction");
Serial.println(direction);
notifyClients(direction);
newRequest = true;
}
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
//Notify client of motor current state when it first connects
notifyClients(direction);
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
void setup() {
// Serial port for debugging purposes
Serial.begin(115200);
initWiFi();
initWebSocket();
initSPIFFS();
myStepper.setSpeed(5);
// Web Server Root URL
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
server.serveStatic("/", SPIFFS, "/");
server.begin();
}
void loop() {
if (newRequest){
if (direction == "CW"){
myStepper.step(steps.toInt());
Serial.print("CW");
}
else{
myStepper.step(-steps.toInt());
}
newRequest = false;
notifyClients("stop");
}
ws.cleanupClients();
}
View raw code
The Arduino sketch is very similar to the previous tutorial, but it handles the client-server communication using WebSocket protocol. Let's see how it works or skip to the demonstration section.
Include Libraries
First, include the required libraries. The WiFi, AsyncTCP, and ESPAsyncWebServer to create the web server, the SPIFFS library to use the ESP32 filesystem, and the Stepper library to control the stepper motor.
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Stepper.h>
Stepper Motor Pins and Steps per Revolution
Define the steps per revolution of your stepper motorin our case, it's 2048:
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
Define the motor input pins. In this example, we're connecting to GPIOs 19, 18, 5, and 17, but you can use any other suitable GPIOs.
#define IN1 19
#define IN2 18
#define IN3 5
#define IN4 17
Initialize an instance of the stepper library called myStepper. Pass as arguments the steps per revolution and the input pins. In the case of the 28BYJ-48 stepper motor, the order of the pins is IN1, IN3, IN2, IN4it might be different for your motor.
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
Network Credentials
Insert your network credentials in the following lines.
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
AsyncWebServer and AsyncWebSocket
Create an AsyncWebServer object called server on port 80.
AsyncWebServer server(80);
The ESPAsyncWebServer library includes a WebSocket plugin that makes it easy to handle WebSocket connections. Create an AsyncWebSocket object called ws to handle the connections on the /ws path.
AsyncWebSocket ws("/ws");
Initializing Variables
The following variables will save the direction and number of steps received via WebSocket protocol. When the program first starts, the motor is stopped.
String direction ="stop";
String steps;
The newRequest variable will be used to check whether a new request occurred. Then, in the loop(), we'll spin the motor when a new request is receivedwhen the newRequest variable is true.
bool newRequest = false;
initSPIFFS()
The initSPIFFS() function initializes the ESP32 Filesystem.
void initSPIFFS() {
if (!SPIFFS.begin(true)) {
Serial.println("An error has occurred while mounting SPIFFS");
}
else{
Serial.println("SPIFFS mounted successfully");
}
}
initWiFi()
The initWiFi() function initializes WiFi.
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
Handling WebSockets Server
Previously, you've seen how to handle the WebSocket connection on the client side (browser). Now, let's take a look on how to handle it on the server side.
Notify All Clients
The notifyClients() function notifies all clients with a message containing whatever you pass as a argument. In this case, we'll want to notify all clients of the current motor state whenever there's a change.
void notifyClients(String state) {
ws.textAll(state);
}
The AsyncWebSocket class provides a textAll() method for sending the same message to all clients that are connected to the server at the same time.
Handle WebSocket Messages
The handleWebSocketMessage() function is a callback function that will run whenever we receive new data from the clients via WebSocket protocol.
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
message = (char*)data;
steps = message.substring(0, message.indexOf("&"));
direction = message.substring(message.indexOf("&")+1, message.length());
Serial.print("steps");
Serial.println(steps);
Serial.print("direction");
Serial.println(direction);
notifyClients(direction);
newRequest = true;
}
}
We split the message to get the number of steps and direction.
message = (char*)data;
steps = message.substring(0, message.indexOf("&"));
direction = message.substring(message.indexOf("&")+1, message.length());
Then, we notify all clients of the motor direction so that all clients change the motor state on the web interface.
notifyClients(direction);
Finally, set the newRequest variable to true, so that the motors starts spinning in the loop().
newRequest = true;
Configure the WebSocket server
Now we need to configure an event listener to handle the different asynchronous steps of the WebSocket protocol. This event handler can be implemented by defining the onEvent() as follows:
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
//Notify client of motor current state when it first connects
notifyClients(direction);
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
The type argument represents the event that occurs. It can take the following values:
WS_EVT_CONNECT when a client has logged in;
WS_EVT_DISCONNECT when a client has logged out;
WS_EVT_DATA when a data packet is received from the client;
WS_EVT_PONG in response to a ping request;
WS_EVT_ERROR when an error is received from the client.
There's a section to notify any client of the current motor state when it first connects:
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
//Notify client of motor current state when it first connects
notifyClients(direction);
break;
Initialize WebSocket
Finally, the initWebSocket() function initializes the WebSocket protocol.
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
setup()
In the setup(), initialize the Serial Monitor.
Serial.begin(115200);
Call the initWiFi() function to initialize WiFi.
initWiFi();
Call the initSPIFFS() function to initialize the filesystem.
initWebSocket();
And set the stepper motor speed in rpm.
myStepper.setSpeed(5);
Handle requests
Then, handle the web server. When you receive a request on the root (/) URLthis is when you access the ESP IP address send the HTML text to build the web page:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", index_html);
});
When the HTML file loads on your browser, it will make a request for the CSS and JavaScript files. These are static files saved on the same directory (SPIFFS). So, we can simply add the following line to serve files in a directory when requested by the root URL. It will serve the CSS and JavaScript files automatically.
server.serveStatic("/", SPIFFS, "/");
Finally, start the server.
server.begin();
loop()
Let's take a look at the loop() section.
If the newRequest variable is true, we'll check what's the spinning direction: CW or CCW. If it is CW, we move the motor the number of steps saved in the steps variable using the step() method on the myStepper object. To move the motor counterclockwise, we just need to pass the number of steps but with a minus () sign.
if (direction == "CW"){
// Spin the stepper clockwise direction
myStepper.step(steps.toInt());
}
else{
// Spin the stepper counterclockwise direction
myStepper.step(-steps.toInt());
}
After spinning the motor, set the newRequest variable to false, so that it can detect new requests again.
newRequest = false;
Additionally, notify all clients that the motor has stopped.
notifyClients("stop");
Upload Code and Files
After inserting your network credentials, save the code. Go to Sketch > Show Sketch Folder, and create a folder called data.
Inside that folder, you should save the HTML, CSS, and JavaScript files.
Then, upload the code to your ESP32 board. Make sure you have the right board and COM port selected. Also, make sure you've added your network credentials.
After uploading the code, you need to upload the files. Go to Tools > ESP32 Data Sketch Upload and wait for the files to be uploaded.
When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the ESP32 EN/RST button, and it should print the ESP32 IP address.
Demonstration
Open a web browser or multiple web browser windows on your local network and you'll access the web page to control the motor. Submit the form to control the motor.
The gear on the web page starts spinning in the right direction and the motor starts working.
When it stops, the gear on the web page and the motor state change accordingly.
Notice that if you have multiple clients connected, all clients update the motor state almost instantaneously.
Watch the video below for a live demonstration.
Wrapping Up
In this tutorial, you've learned how to control a stepper motor using a web server built with the ESP32. The web server provides a web page to control the stepper motor using a form whose results are sent to the ESP32 via WebSocket protocol.
This is part 3 of a series of tutorials about controlling a stepper motor using a web server. You can follow Part 1 and 2 at the following link:
Control Stepper Motor with ESP32 Web Server (HTML Form)
Web Server: Control Stepper Motor (HTML Form)
In this tutorial, you'll learn how to create a web server with the ESP32 to control a stepper motor remotely. The web server displays a web page with an HTML form that allows you to select the direction and number of steps you want the motor to move.
Table of Contents
Control Stepper Motor with HTML Form (minimal setup)
Control Stepper Motor with HTML Form + CSS (using SPIFFS)
Control Stepper Motor with WebSockets (HTML, CSS, JavaScript)
In the picture below, you can see the three web server projects we'll build (number 3 is in this post).
This is a didactic tutorial where you'll learn more about creating web pages and interaction between the ESP32 and the client. We'll show you how to create the web page step-by-step with HTML and send the form results to the ESP32 via HTTP POST to control the stepper motor.
Later, you'll add some CSS to style the web page to improve its look.
Finally, we'll show you how to use Websockets for bidirectional communication between the server and the client. This will allow us to know on the web interface whether the motor is spinning or stopped. This section will add some JavaScript to handle WebSocket communication and add some cool animations to the web page.
The following articles might be useful to understand the concepts covered throughout this tutorial:
ESP32 with Stepper Motor (28BYJ-48 and ULN2003 Motor Driver)
Input Data on HTML Form ESP32/ESP8266 Web Server using Arduino IDE
ESP32 WebSocket Server: Control Outputs (Arduino IDE)
Prerequisites
Before proceeding with the tutorial, make sure you check the following prerequisites.
1) Parts Required
To follow this tutorial, you need the following parts:
28BYJ-48 Stepper Motor + ULN2003 Motor Driver
ESP32 (read Best ESP32 Development Boards)
Jumper Wires
5V Power Supply
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
2) Arduino IDE and ESP32 Boards Add-on
We'll program the ESP32 using Arduino IDE. So, you must have the ESP32 add-on installed. Follow the next tutorial if you haven't already:
Installing ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP32:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
3) Filesystem Uploader Plugin
To upload the HTML, CSS, and JavaScript files needed to build this project to the ESP32 filesystem (SPIFFS), we'll use a plugin for Arduino IDE: SPIFFS Filesystem uploader. Follow the next tutorial to install the filesystem uploader plugin if you haven't already:
ESP32: Install SPIFFS FileSystem Uploader Plugin in Arduino IDE
If you're using VS Code with the PlatformIO extension, read the following tutorial to learn how to upload files to the filesystem:
ESP32 with VS Code and PlatformIO: Upload Files to Filesystem (SPIFFS)
4) Libraries
To build this project, you need to install the following libraries:
ESPAsyncWebServer (.zip folder)
AsyncTCP (.zip folder)
The ESPAsyncWebServer and AsynTCP libraries aren't available to install through the Arduino Library Manager. You need to click on the previous links to download the library files. Then, in your Arduino IDE, go to Sketch> Include Library > Add .zip Library and select the libraries you've just downloaded.
Installing Libraries (VS Code + PlatformIO)
If you're programming the ESP32 using PlatformIO, you should add the following lines to the platformio.ini file to include the libraries (also change the Serial Monitor speed to 115200):
monitor_speed=115200
lib_deps = ESP Async WebServer
arduino-libraries/Arduino_JSON @ 0.1.0
arduino-libraries/Stepper @ ^1.1.3
5) Schematic Diagram
The following schematic diagram shows the connections between the stepper motor to the ESP32.
Note: You should power the motor driver using an external 5V power supply.
Motor Driver |
ESP32 |
IN1 |
GPIO 19 |
IN2 |
GPIO 18 |
IN3 |
GPIO 5 |
IN4 |
GPIO 17 |
1. Control Stepper Motor with HTML Form
In this section, you'll learn how to create a simple HTML form to control the stepper motor.
Here's how it works:
On the web page, you can select whether you want the motor to turn Clockwise or Counterclockwise. Those are radio buttons. Radio buttons are usually displayed as small circles, which are filled or highlighted when selected. You can only select one radio button in a given group at a time.
There is an input field of type number where the user can enter a numberin this case, the number of steps.
Finally, a button called GO! of type submit sends the data to the server via an HTTP POST request.
HTML Form and Input Fields
In this section, we'll take a look at the HTML to build the form.
In HTML, the <form> tag is used to create an HTML form to collect user input. The user input is then sent to the server (ESP32 or ESP8266) for processing. Based on the values collected on the form, your ESP board may perform different actionsin this case, spin the motor a determined number of steps.
Here's the HTML we'll use for this project.
<!DOCTYPE html>
<html>
<head>
<title>Stepper Motor</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Stepper Motor Control</h2>
<form action="/" method="POST">
<input type="radio" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
<label for="steps">Number of steps:</label>
<input type="number" name="steps">
<input type="submit" value="GO!">
</form>
</body>
</html>
An HTML form contains different form elements. All form elements are enclosed inside the <form> tag. It contains controls <input> (radio buttons and number input field) and labels for those controls (<label>).
Additionally, the <form> tag must include the action attribute that specifies what you want to do when the form is submitted. In our case, we want to send that data to the server (ESP32/ESP8266) when the user clicks the submit button. The method attribute specifies the HTTP method (GET or POST) used when submitting the form data.
<form action="/" method="POST">
POST is used to send data to a server to create/update a resource. The data sent to the server with POST is stored in the body of the HTTP request.
Radio Buttons
A radio button is defined as follows:
<input type="radio">
For our project, we need two radio buttons, and only one can be selected at a time. So, we can create a group of radio buttons. To do that, the radio buttons must share the same name (the value of the name attributein this case direction).
<input type="radio" name="direction">
Finally, we also need the value attribute that specifies a unique value for each radio button. This value is not visible to the user, but it is sent to the server when you click on the submit button to identify which button was selected.
In our example, we created one radio button with the value CW (to select clockwise) and another CCW (to select counterclockwise).
<input type="radio" name="direction" value="CW">
<input type="radio" name="direction" value="CCW">
Finally, if you want one radio button to be selected by default, you can add the keyword checked. In our example, the clockwise direction is selected by default.
<input type="radio" name="direction" value="CW" checked>
So, this is how the radio buttons and corresponding labels look like:
<input type="radio" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
Input Field
Finally, we also need an input field where the user enters the number of stepsan input field of type number. The name attribute allows us to determine in which input field the user entered the data.
<input type="number" name="steps">
Submit Button
To complete the form, we need a submit button. A submit button is an input of type submit. When you click this button, the form's data is sent to the server (the ESP32 or ESP8266 boards). The value attribute specifies the text to display on the button.
<input type="submit" value="GO!">
For example, if you select the clockwise direction and enter 2000 steps, the client will make the following request to the ESP:
POST /
Host: localhost
direction=CW&steps=2000
The ESP receives this request and can get the direction and number of steps from the body of the request.
Code
Now that you know how to create the HTML form, let's go through the Arduino code.
The HTML text can be saved on an HTML file saved on the ESP32 filesystem (SPIFFS) or it can be saved in a variable in your Arduino sketch.
Because the HTML text for this example is relatively simple and we don't have CSS or JavaScript files, we'll save the HTML text as a variable (index_html).
Here's the code to build the web server (insert your network credentials and the code will work straight away).
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/stepper-motor-esp32-web-server/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Stepper.h>
// Stepper Motor Settings
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
#define IN1 19
#define IN2 18
#define IN3 5
#define IN4 17
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
// Search for parameters in HTTP POST request
const char* PARAM_INPUT_1 = "direction";
const char* PARAM_INPUT_2 = "steps";
// Variables to save values from HTML form
String direction;
String steps;
// Variable to detect whether a new request occurred
bool newRequest = false;
// HTML to build the web page
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Stepper Motor</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Stepper Motor Control</h2>
<form action="/" method="POST">
<input type="radio" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
<label for="steps">Number of steps:</label>
<input type="number" name="steps">
<input type="submit" value="GO!">
</form>
</body>
</html>
)rawliteral";
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
initWiFi();
myStepper.setSpeed(5);
// Web Server Root URL
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", index_html);
});
// Handle request (form)
server.on("/", HTTP_POST, [](AsyncWebServerRequest *request) {
int params = request->params();
for(int i=0;i<params;i++){
AsyncWebParameter* p = request->getParam(i);
if(p->isPost()){
// HTTP POST input1 value (direction)
if (p->name() == PARAM_INPUT_1) {
direction = p->value().c_str();
Serial.print("Direction set to: ");
Serial.println(direction);
}
// HTTP POST input2 value (steps)
if (p->name() == PARAM_INPUT_2) {
steps = p->value().c_str();
Serial.print("Number of steps set to: ");
Serial.println(steps);
}
}
}
request->send(200, "text/html", index_html);
newRequest = true;
});
server.begin();
}
void loop() {
// Check if there was a new request and move the stepper accordingly
if (newRequest){
if (direction == "CW"){
// Spin the stepper clockwise direction
myStepper.step(steps.toInt());
}
else{
// Spin the stepper counterclockwise direction
myStepper.step(-steps.toInt());
}
newRequest = false;
}
}
View raw code
How the Code Works
Continue reading to learn how the code works, or skip to the demonstration section.
Include Libraries
First, include the required libraries. The WiFi, AsyncTCP, and ESPAsyncWebServer to create the web server and the Stepper library to control the stepper motor.
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Stepper.h>
Stepper Motor Pins and Steps per Revolution
Define the steps per revolution of your stepper motorin our case, it's 2048:
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
Define the motor input pins. In this example, we're connecting to GPIOs 19, 18, 5, and 17, but you can use any other suitable GPIOs.
#define IN1 19
#define IN2 18
#define IN3 5
#define IN4 17
Initialize an instance of the stepper library called myStepper. Pass as arguments the steps per revolution and the input pins. In the case of the 28BYJ-48 stepper motor, the order of the pins is IN1, IN3, IN2, IN4it might be different for your motor.
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
Network Credentials
Insert your network credentials in the following lines.
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Create an AsyncWebServer object called server on port 80.
AsyncWebServer server(80);
Initializing Variables
The PARAM_INPUT_1 and PARAM_INPUT_2 variables will be used to search for parameters in the HTTP POST request. Remember that it contains the direction and number of steps.
// Search for parameters in HTTP POST request
const char* PARAM_INPUT_1 = "direction";
const char* PARAM_INPUT_2 = "steps";
The following variables will save the direction and number of steps.
String direction;
String steps;
The newRequest variable will be used to check whether a new request occurred. Then, in the loop(), we'll spin the motor when a new request is receivedwhen the newRequest variable is true.
bool newRequest = false;
HTML Form
The index_html variable saves the HTML text to build the web pagewe've seen previously how the HTML to build the web page with the form works.
// HTML to build the web page
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Stepper Motor</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Stepper Motor Control</h2>
<form action="/" method="POST">
<input type="radio" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
<label for="steps">Number of steps:</label>
<input type="number" name="steps">
<input type="submit" value="GO!">
</form>
</body>
</html>
)rawliteral";
initWiFi()
The initWiFi() function initializes WiFi.
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
In this example, the ESP is set as a Wi-Fi station (it connects to your router). If you don't have a router nearby, you can set your board as an Access Point. You can read the next tutorial to learn how to set the board as an access point:
How to Set an ESP32 Access Point (AP) for Web Server
setup()
In the setup(), initialize the Serial Monitor.
Serial.begin(115200);
Call the initWiFi() function to initialize WiFi.
initWiFi();
And set the stepper motor speed in rpm.
myStepper.setSpeed(5);
Handle requests
Then, handle the web server. When you receive a request on the root (/) URLthis happens when you access the ESP IP addresssend the HTML text to build the web page:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", index_html);
});
Then, you need to handle what happens when the ESP receives a POST request with the form details.
server.on("/", HTTP_POST, [](AsyncWebServerRequest *request) {
First, we search for parameters in the HTTP POST request:
int params = request->params();
for(int i=0;i<params;i++){
AsyncWebParameter* p = request->getParam(i);
if(p->isPost()){
If one of the parameters is equal to PARAM_INPUT_1, we know its value contains the direction of the motor. If that's the case, we get the value of that parameter and save it in the direction variable.
if (p->name() == PARAM_INPUT_1) {
direction = p->value().c_str();
Serial.print("Direction set to: ");
Serial.println(direction);
}
We follow a similar procedure for PARAM_INPUT_2, but we save the value in the steps variable.
if (p->name() == PARAM_INPUT_2) {
steps = p->value().c_str();
Serial.print("Number of steps set to: ");
Serial.println(steps);
}
Finally, we respond with the content of the HTML pageit will reload the page.
request->send(200, "text/html", index_html);
After this, we set the newRequest variable to true, so that it spins the motor in the loop().
newRequest = true;
loop()
Let's take a look at the loop() section.
If the newRequest variable is true, we'll check what's the spinning direction: CW or CCW. If it is CW, we move the motor the number of steps saved in the steps variable using the step() method on the myStepper object.
To move the motor counterclockwise, we just need to pass the number of steps but with a minus sign.
if (direction == "CW"){
// Spin the stepper clockwise direction
myStepper.step(steps.toInt());
}
else{
// Spin the stepper counterclockwise direction
myStepper.step(-steps.toInt());
}
After spinning the motor, set the newRequest variable to false, so that it can detect new requests again.
newRequest = false;
Note: because the Stepper.h is not an asynchronous library, it won't do anything else until the motor has stopped spinning. So, if you try to make new requests while the motor is spinning, it will not work. We'll build an example using WebSocket protocol that will allow us to know on the web interface whether the motor is spinning or notyou can check that tutorial here.
Demonstration
After inserting your network credentials, you can upload the code to your board.
After uploading, open the Serial Monitor at a baud rate of 115200 and press the on-board RESET button. The ESP IP address will be displayed.
Open a browser on your local network and insert the ESP IP address. You'll get access to the HTML form to control the stepper motor.
Select the direction and enter a determined number of steps. Then, press GO!. The stepper motor will start spinning.
At the same time, you can see the values of the direction and steps variables on the Serial Monitor.
2. Styling the Form with CSS
In the previous section, we've created a plain form without any formatting. By adding some CSS to your project, your HTML page will look much better.
When your HTML also includes CSS, it is easier to work if you have separated HTML and CSS files (apart from the Arduino sketch file). So, instead of writing HTML and CSS in the Arduino sketch, we'll create separated HTML and CSS files.
These files will then be uploaded to the ESP32 filesystem (SPIFFS) using the SPIFFS Filesystem uploader plugin.
Organizing Your Files
The files you want to upload to the ESP filesystem should be placed in a folder called data under the project folder. We'll move two files to that folder:
index.html to build the web page
style.css to style the web page
You should save the HTML, CSS, and JavaScript files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We'll upload these files to the ESP32 filesystem (SPIFFS).
You can download all project files:
Download All the Arduino Project Files
Page Overview
To better understand how styling the web page works, let's take a closer look at the web page we'll build.
HTML File
We need to make some modifications to the HTML file to make it easier to format using CSS. Create a file called index.html and copy the following into that file.
<!DOCTYPE html>
<html>
<head>
<title>Stepper Motor</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
</head>
<body>
<div>
<h1>Stepper Motor Control <i></i></h2>
</div>
<div>
<form action="/" method="POST">
<input type="radio" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
<label for="steps">Number of steps:</label>
<input type="number" name="steps">
<input type="submit" value="GO!">
</form>
</div>
</body>
</html>
View raw code
To use a CSS file to style the HTML page, you need to reference the style sheet in your HTML document. So you need to add the following between the <head> tags of your document:
<link rel="stylesheet" type="text/css" href="stylesheet.css">
This <link> tag tells the HTML file that you're using an external style sheet to format how the page looks. The rel attribute specifies the nature of the external file. In this case, it is a style sheetthe CSS filethat will be used to alter the page's appearance.
The type attribute is set to text/css to indicate that you're using a CSS file for the styles. The href attribute indicates the file location; since the file is in the same folder as the HTML file, you just need to reference the filename. Otherwise, you need to reference its file path.
Now you have your style sheet connected to your HTML document.
To use the fontawesome icons in the web page like the cogs, we need to add the following line.
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
We created a <div> tag with the class topnav to make it easier to format the first heading.
<div>
<h1>Stepper Motor Control <i></i></h2>
</div>
Then, we include the form inside a <div> tag with the class content. This will make it easier to format the area occupied by the form.
<div>
CSS File
Create a file called style.css with the following content to format the form.
html {
font-family: Arial, Helvetica, sans-serif;
}
h1 {
font-size: 1.8rem;
color: white;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
text-align: center;
}
body {
margin: 0;
}
.content {
padding: 20px;
max-width: max-content;
margin: 0 auto;
}
form{
border-radius: 5px;
background-color: #f2f2f2;
padding: 20px;
}
input[type=number], select {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
input[type=submit] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #034078;
border: none;
padding: 14px 20px;
text-align: center;
font-size: 20px;
border-radius: 4px;
transition-duration: 0.4s;
width: 100%;
color: white;
cursor: pointer;
}
input[type=submit]:hover {
background-color: #1282A2;
}
input[type="radio"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 50%;
width: 16px;
height: 16px;
border: 2px solid #999;
transition: 0.2s all linear;
margin-right: 5px;
position: relative;
top: 4px;
}
input[type="radio"]:checked{
border: 6px solid #1282A2;
}
View raw code
The html selector includes the styles that apply to the whole HTML page. In this case, we're setting the font.
html {
font-family: Arial, Helvetica, sans-serif;
}
The h1 selector includes the styles for heading 1. In our case, the heading 1 includes the text Stepper Motor Control. This sets the text font size and color.
h1 {
font-size: 1.8rem;
color: white;
}
To select the <div> with the topnav class, use a dot (.) before the class name, like this:
.topnav {
Set the .topnav background color using the background-color property. You can choose any background color. We're using #0A1128. The text is aligned at the center. Additionally, set the overflow property to hidden like this:
.topnav {
overflow: hidden;
background-color: #0A1128;
text-align: center;
}
It's a bit difficult to explain what the overflow property does. The best way to understand it is to render your web page with and without that property to spot the differences.
The margin of the <body>the container that includes the whole HTML pageis set to 0 so that it occupies all the browser window space.
body {
margin: 0;
}
The following lines style the content div (that contains the form): padding and margin. Additionally, set its max-width to the maximum width of its content (the form itself).
.content {
padding: 20px;
max-width: max-content;
margin: 0 auto;
}
The form is a container with round borders (border-radius property) and light gray background color (background-color property). We also add some padding.
form{
border-radius: 5px;
background-color: #f2f2f2;
padding: 20px;
}
Then, we need to style each individual element of the form. To select the input number, we use the input[type=number] selector.
input[type=number], select {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
Note: the [attribute=value] selector selects elements with the specified attribute and value. In this case, we're selecting the input elements of type number.
To style the submit button, use the input[type=submit] selector.
input[type=submit] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #034078;
border: none;
padding: 14px 20px;
text-align: center;
font-size: 20px;
border-radius: 4px;
transition-duration: 0.4s;
width: 100%;
color: white;
cursor: pointer;
}
To make the button change color when you hover your mouse over it, you can use the :hover selector.
input[type=submit]:hover {
background-color: #1282A2;
}
Finally, to select the radio buttons, use the input[type=radio] selector.
input[type="radio"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 50%;
width: 16px;
height: 16px;
border: 2px solid #999;
transition: 0.2s all linear;
margin-right: 5px;
position: relative;
top: 4px;
}
To style the selected radio button, you can use the :checked selector.
input[type="radio"]:checked{
border: 6px solid #1282A2;
}
The form elements were styled based on an example provided by the W3Schools website. If you want to better understand how it works, you can check it here.
Arduino Sketch
In this project, the HTML and CSS files are saved in the ESP32 filesystem (SPIFFS). So, we need to make some modifications to the sketch.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/stepper-motor-esp32-web-server/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>
#include <Stepper.h>
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
#define IN1 19
#define IN2 18
#define IN3 5
#define IN4 17
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
// Search for parameter in HTTP POST request
const char* PARAM_INPUT_1 = "direction";
const char* PARAM_INPUT_2 = "steps";
//Variables to save values from HTML form
String direction;
String steps;
bool newRequest = false;
// Initialize SPIFFS
void initSPIFFS() {
if (!SPIFFS.begin(true)) {
Serial.println("An error has occurred while mounting SPIFFS");
}
else {
Serial.println("SPIFFS mounted successfully");
}
}
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
// Serial port for debugging purposes
Serial.begin(115200);
initWiFi();
initSPIFFS();
myStepper.setSpeed(5);
// Web Server Root URL
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
server.serveStatic("/", SPIFFS, "/");
server.on("/", HTTP_POST, [](AsyncWebServerRequest *request) {
int params = request->params();
for(int i=0;i<params;i++){
AsyncWebParameter* p = request->getParam(i);
if(p->isPost()){
// HTTP POST input1 value
if (p->name() == PARAM_INPUT_1) {
direction = p->value().c_str();
Serial.print("Direction set to: ");
Serial.println(direction);
}
// HTTP POST input2 value
if (p->name() == PARAM_INPUT_2) {
steps = p->value().c_str();
Serial.print("Number of steps set to: ");
Serial.println(steps);
// Write file to save value
}
newRequest = true;
//Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
}
}
request->send(SPIFFS, "/index.html", "text/html");
});
server.begin();
}
void loop() {
if (newRequest){
if (direction == "CW"){
myStepper.step(steps.toInt());
Serial.print("CW");
}
else{
myStepper.step(-steps.toInt());
}
newRequest = false;
}
}
View raw code
Let's take a look at the modifications you need to make.
First, you need to include the SPIFFS.h library.
#include "SPIFFS.h"
Then, you need to initialize SPIFFS. We created a function called initSPIFFS() to do that.
void initSPIFFS() {
if (!SPIFFS.begin(true)) {
Serial.println("An error has occurred while mounting SPIFFS");
}
else {
Serial.println("SPIFFS mounted successfully");
}
}
Then, you need to call that function in the setup() before initializing the web server.
initSPIFFS();
Then, to handle requests, you need to indicate that your HTML file is saved in SPIFFS, as follows:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
When the HTML file loads on your browser, it will make a request for the CSS file. This is a static file saved on the same directory (SPIFFS). So, we can simply add the following line to serve static files in a directory when requested by the root URL. It will serve the CSS file automatically.
server.serveStatic("/", SPIFFS, "/");
Upload Code and Files
Before uploading, you can use the following link to:
Download All the Arduino Project Files
After inserting your network credentials, save the code. Go to Sketch > Show Sketch Folder, and create a folder called data.
Inside that folder, you should save the HTML and CSS files.
Then, upload the code to your ESP32 board. Make sure you have the right board and COM port selected. Also, make sure you've added your network credentials.
After uploading the code, you need to upload the files. Go to Tools > ESP32 Data Sketch Upload and wait for the files to be uploaded.
When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the ESP32 EN/RST button, and it should print the ESP32 IP address.
Demonstration
Open a browser on your local network and paste the ESP32 IP address. You'll get access to the HTML form to control the stepper motor. This works similarly to the example of the previous section, but with a better look.
Wrapping Up
This tutorial is already quite long. So, we'll include the third part of this tutorial in a separate publication. In that third part, the ESP32 and the client communicate using WebSocket protocol and the web page shows whether the motor is spinning or stopped. We've also included an animation with some gears spinning in the same direction as the motor.
Continue to PART 3 ESP32 Web Server: Control Stepper Motor (WebSocket). Sneak peek at the third part in the video below.
Door Status Monitor with Email Notifications (IFTTT)
In this project, you're going to monitor the status of a door using an ESP32 board and a magnetic reed switch. You'll receive an email notification whenever the door changes state: opened or closed. The email notifications will be sent using IFTTT, and the ESP32 board will be programmed using Arduino IDE.
Instead of sending email notifications with IFTTT, you can use an SMTP server instead. To learn how to send emails with the ESP32 using an SMTP server, you can follow the next tutorial:
ESP32 Send Emails using an SMTP Server: HTML, Text, and Attachments (Arduino IDE)
If you prefer, you can also send notifications to your Telegram account. Learn how to use Telegram with the ESP32 on the following tutorial:
Telegram: ESP32 Motion Detection with Notifications (Arduino IDE)
We have a similar tutorial for the ESP8266 board: Door Status Monitor with Email Notifications (IFTTT)
Project Overview
In this project, you'll send an email notification whenever a door changes state. To detect the change, we'll use a magnetic contact switch. To send an email, we'll use IFTTT.
A magnetic contact switch is basically a reed switch encased in a plastic shell so that you can easily apply it to a door, a window, or a drawer to detect if the door is open or closed.
The electrical circuit is closed when a magnet is near the switchdoor closed. When the magnet is far away from the switchdoor openthe circuit is open. See the figure below.
We can connect the reed switch to an ESP32 GPIO to detect changes in its state.
Sending Emails with IFTTT
To send emails with the ESP32, we'll use a free* service called IFTTT, which stands for If This Then That.
IFTTT is a platform that gives you creative control over dozens of products and apps. You can make apps work together. For example, sending a particular request to IFTTT triggers an applet that makes something happen, like sending you an email alert.
I like IFTTT service and once you understand how it works, it is easy to use. However, I'm not too fond of the layout of their website and the fact that it is constantly changing.
* currently, you can have three active applets simultaneously in the free version.
Creating an IFTTT Account
Creating an account on IFTTT is free!
Go to the official site: https://ifttt.com/ and click the Get Started button at the top of the page or Signup if you already have an account.
Creating an Applet
First, you need to create an Applet in IFTTT. An Applet connects two or more apps or devices together (like the ESP32 and sending an email).
Applets are composed of triggers and actions:
Triggers tell an Applet to start. The ESP32 will send a request (webhooks) that will trigger the Applet.
Actions are the end result of an Applet run. In our case, sending an email.
Follow the next instructions to create your applet.
1) Click on this link to start creating an Applet.
2) Click on the Add button.
3) Search for Webhooks and select it.
4) Select the option Receive a web request.
5) Enter the event name, for example, door_status. You can call it any other name, but if you change it, you'll also need to change it in the code provided later on.
6) Then, you need to click the Add button on the Then that menu to select what happens when the previous event is triggered.
7) Search for email and select the email option.
8) Click on Send me an email.
9) Then, write the email subject and body. You can leave the default message or change it to whatever you want. The {{EventName}} is a placeholder for the event name, in this case, it's door_status. The {{OccuredAt}} is a placeholder for the timestamp of when the event was triggered. The {{Value1}} is a placeholder for the actual door status. So, you can play with those placeholders to write your own message. When you're done, click on Create action.
10) Now, you can click on Continue.
11) Finally, click on Finish.
12) You'll be redirected to a similar pageas shown below.
Your Applet was successfully created. Now, let's test it.
Testing your Applet
Go to this URL: https://ifttt.com/maker_webhooks and open the Documentation tab.
You'll access a web page where you can trigger an event to test it and get access to your API key (highlighted in red). Save your API key to a safe place because you'll need it later.
Now, let's trigger the event to test it. In the {event} placeholder, write the event you created previously. In our case, it is door_status. Additionally, add a value in the value1 field, for example open. Then, click the Test It button.
You should get a success message saying Event has been triggered and you should get an email in your inbox informing you that the event has been triggered.
If you received the email, your Applet is working as expected. You can proceed to the next section. We'll program the ESP32 to trigger your Applet when the door changes state.
Parts List
Here's the hardware that you need to complete this project:
ESP32 read Best ESP32 Development Boards
1 Magnetic Reed Switch
1 10k resistor
1 breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic ESP32 with Reed Switch
We wired the reed switch to GPIO 4, but you can connect it to any suitable GPIO.
Code
Copy the sketch below to your Arduino IDE. Replace the SSID, password, and the IFTTT API Key with your own credentials.
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-door-status-monitor-email/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/
#include <WiFi.h>
// Set GPIOs for LED and reedswitch
const int reedSwitch = 4;
const int led = 2; //optional
// Detects whenever the door changed state
bool changeState = false;
// Holds reedswitch state (1=opened, 0=close)
bool state;
String doorState;
// Auxiliary variables (it will only detect changes that are 1500 milliseconds apart)
unsigned long previousMillis = 0;
const long interval = 1500;
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
const char* host = "maker.ifttt.com";
const char* apiKey = "REPLACE_WITH_YOUR_IFTTT_API_KEY";
// Runs whenever the reedswitch changes state
ICACHE_RAM_ATTR void changeDoorStatus() {
Serial.println("State changed");
changeState = true;
}
void setup() {
// Serial port for debugging purposes
Serial.begin(115200);
// Read the current door state
pinMode(reedSwitch, INPUT_PULLUP);
state = digitalRead(reedSwitch);
// Set LED state to match door state
pinMode(led, OUTPUT);
digitalWrite(led, !state);
// Set the reedswitch pin as interrupt, assign interrupt function and set CHANGE mode
attachInterrupt(digitalPinToInterrupt(reedSwitch), changeDoorStatus, CHANGE);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
}
void loop() {
if (changeState){
unsigned long currentMillis = millis();
if(currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
// If a state has occured, invert the current door state
state = !state;
if(state) {
doorState = "closed";
}
else{
doorState = "open";
}
digitalWrite(led, !state);
changeState = false;
Serial.println(state);
Serial.println(doorState);
//Send email
Serial.print("connecting to ");
Serial.println(host);
WiFiClient client;
const int httpPort = 80;
if (!client.connect(host, httpPort)) {
Serial.println("connection failed");
return;
}
String url = "/trigger/door_status/with/key/";
url += apiKey;
Serial.print("Requesting URL: ");
Serial.println(url);
client.print(String("POST ") + url + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Content-Length: 13\r\n\r\n" +
"value1=" + doorState + "\r\n");
}
}
}
View raw code
You must have the ESP32 board add-on installed in your Arduino IDE. If you don't, follow the next tutorial:
How to Install the ESP32 Board in Arduino IDE
How the Code Works
Continue reading to learn how the code works, or proceed to the Demonstration section.
First, you need to include the WiFi library so that the ESP32 can connect to your network to communicate with the IFTTT services.
#include <WiFi.h>
Set the GPIOs for the reed switch and LED (the on-board LED is GPIO 2). We'll light up the on-board LED when the door is open.
const int reedSwitch = 4;
const int led = 2; //optional
The changeState boolean variable indicates whether the door has changed state.
bool changeState = false;
The state variable will hold the reed switch state and the doorState, as the name suggests, will hold the door stateclosed or opened.
bool state;
String doorState;
The following timer variables allow us to debounce the switch. Only changes that have occurred with at least 1500 milliseconds between them will be considered.
unsigned long previousMillis = 0;
const long interval = 1500;
Insert your SSID and password in the following variables so that the ESP32 can connect to the internet.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Insert your own IFTTT API key on the apiKey variablethe one you've gotten in this step.
const char* apiKey = "REPLACE_WITH_YOUR_IFTTT_API_KEY";
The changeDoorStatus() function will run whenever a change is detected on the door state. This function simply changes the changeState variable to true. Then, in the loop() we'll handle what happens when the state changes (invert the previous door state and send an email).
ICACHE_RAM_ATTR void changeDoorStatus() {
Serial.println("State changed");
changeState = true;
}
setup()
In the setup(), initialize the Serial Monitor for debugging purposes:
Serial.begin(115200);
Set the reed switch as an INPUT. And save the current state when the ESP32 first starts.
pinMode(reedSwitch, INPUT_PULLUP);
state = digitalRead(reedSwitch);
Set the LED as an OUTPUT and set its state to match the reed switch state (circuit closed and LED off; circuit opened and LED on).
pinMode(led, OUTPUT);
digitalWrite(led, !state);
Setting an interrupt
Set the reed switch as an interrupt.
attachInterrupt(digitalPinToInterrupt(reedSwitch), changeDoorStatus, CHANGE);
To set an interrupt in the Arduino IDE, you use the attachInterrupt() function, which accepts as arguments: the GPIO interrupt pin, the name of the function to be executed, and mode.
The first argument is a GPIO interrupt. You should use digitalPinToInterrupt(GPIO) to set the actual GPIO as an interrupt pin.
The second argument of the attachInterrupt() function is the name of the function that will be called every time the interrupt is triggered the interrupt service routine (ISR). In this case, it is the changeDoorStatus function.
The ISR function should be as simple as possible, so the processor gets back to the execution of the main program quickly.
The third argument is the mode. We set it to CHANGE to trigger the interrupt whenever the pin changes value for example from HIGH to LOW or LOW to HIGH.
To learn more about interrupts with the ESP32, read the following tutorial:
ESP32 Interrupts and Timers using Arduino IDE
Initialize Wi-Fi
The following lines connect the ESP32 to Wi-Fi.
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
loop()
In the loop(), we'll read the changeState variable and if a change has occurred, we'll send an email using IFTTT.
First, check if a change occurred:
if (changeState){
Then, check if at least 1500 milliseconds have passed since the last state change.
if(currentMillis - previousMillis >= interval) {
If that's true, reset the timer and invert the current switch state:
state = !state;
If the reed switch state is 1(true), the door is closed. So, we change the doorState variable to closed.
if(state) {
doorState = "closed";
}
If it's 0(false), the door is opened.
else{
doorState = "open";
}
Set the LED state accordingly and print the door state in the Serial Monitor.
digitalWrite(led, !state);
changeState = false;
Serial.println(state);
Serial.println(doorState);
Finally, the following lines make a request to IFTTT with the current door status on the event (door_status) that we created previously.
// Send email
Serial.print("connecting to ");
Serial.println(host);
WiFiClient client;
const int httpPort = 80;
if (!client.connect(host, httpPort)) {
Serial.println("connection failed");
return;
}
String url = "/trigger/door_status/with/key/";
url += apiKey;
Serial.print("Requesting URL: ");
Serial.println(url);
client.print(String("POST ") + url + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Content-Length: 13\r\n\r\n" +
"value1=" + doorState + "\r\n");
When the IFTTT receives this request, it will trigger the action to send an email.
Demonstration
After modifying the sketch to include your network credentials and API key, upload it to your ESP32. Go to Tools > Board and select your ESP32 board. Then, go to Tools > Port and select the COM port the ESP32 is connected to.
Open the Serial Monitor at a baud rate of 115200 to check if the changes are detected and if the ESP32 can connect to IFTTT.
For prototyping/testing you can apply the magnetic reed switch to your door using Velcro.
Now when someone opens/closes your door you get notified via email.
Wrapping Up
In this tutorial you've learned how to trigger an event when the reed switch changes state. This can be useful to detect if a door, window, or drawer was opened or closed. You've also learned how to use IFTTT to send an email when an event is triggered.
Instead of sending an email, you may want to send a message to Telegram, for example.
with Firebase Creating a Web App
This guide will teach you how to create a simple Firebase Web App to control and monitor your ESP32 board. The Web App you'll create can be accessed worldwide to control and monitor your ESP32 from anywhere in the world. This Web App makes the bridge between the Firebase Realtime Database and the ESP32.
Complete the following tutorial before proceeding:
Getting Started with ESP32 with Firebase (Realtime Database)
Here are the major steps to complete this tutorial.
Creating Firebase Projectwe recommend using the Firebase project from this previous tutorial.
Installing Required Software
Setting Up a Firebase Web App Project (VS Code)
Creating Firebase Web App
We have a similar tutorial for the ESP8266 board: ESP8266 NodeMCU with Firebase Creating a Web App
Installing Required Software
To follow this project, you need to install the following software:
Visual Studio Code
Node.JS LTS version
Install Firebase Tools
Installing VS Code
Follow the next instructions to install VS Code on your Operating System:
A) Windows
B) MacOS X
C) Linux
A) Installing VS Code on Windows (Visual Studio Code)
Go to https://code.visualstudio.com/ and download the stable build for your operating system (Windows).
Click on the installation wizard to start the installation and follow all the steps to complete the installation. Accept the agreement and press the Next button.
Select the following options and click Next.
Press the Install button.
Finally, click Finish to finish the installation.
Open VS Code, and you'll be greeted by a Welcome tab with the released notes of the newest version.
That's it. Visual Studio Code was successfully installed.
B) Installing VS Code on Mac OS X (Visual Studio Code)
Go to https://code.visualstudio.com/ and download the stable build for your operating system (Mac OS X).
After downloading the Visual Studio Code application file, you'll be prompted with the following message. Press the Open button.
Or open your Downloads folder and open Visual Studio Code.
After that, you'll be greeted by a Welcome tab with the released notes of the newest version.
That's it. Visual Studio Code was successfully installed.
C) Installing VS Code on Linux Ubuntu (Visual Studio Code)
Go to https://code.visualstudio.com/ and download the stable build for your operating system (Linux Ubuntu).
Save the installation file:
To install it, open a Terminal window, navigate to your Downloads folder and run the following command to install VS Code.
$ cd Downloads
~/Downloads $ sudo apt install ./code_1.49.1-1600299189_amd64.deb
When the installation is finished, VS Code should be available in your applications menu.
Open VS Code, and you'll be greeted by a Welcome tab with the released notes of the newest version.
That's it. Visual Studio Code was successfully installed.
Installing Node.js
1) Go to nodejs.org and download the LTS version.
2) Run the executable file and follow the installation process.
3) Enable automatically install all the necessary tools.
4) When it's done, click Finish.
5) A Terminal window will open to install the Additional Tools for Node.js. When it's done, click any key to continue. When it's finished, you can close the Terminal Window.
Installing Firebase Tools (VS Code)
1) Open VS Code. Close all opened projects, if any.
2) Open a new Terminal window. Go to Terminal > New Terminal.
3) Run the following command to change to the C:\ path (you can install it in any other path):
cd \
Before installing Firebase tools, run the following command to install the latest npm package:
npm install -g npm@latest
4) Run the following command to install firebase tools globally:
npm -g install firebase-tools
5) Firebase tools will be installed, and you'll get a similar message on the Terminal window (you can ignore any warning about deprecated libraries).
6) Test if Firebase was successfully installed with the following command:
firebase --version
In my case, I get the following error.
As you can see in the error message, there's an error message related to the firebase.ps1 file on the C: \Users\username\AppData\Roaming\npm path.
Go to that path and delete the firebase.ps1 file.
Go back to VS Code, and rerun the following command.
firebase --version
This time, it should return the Firebase Tools version without any error.
Setting Up a Firebase Web App Project (VS Code)
Before creating the Firebase Web App, you need to set up a Firebase Project on VS Code. These are the steps:
Creating a Project Folder
Firebase Login
Initializing Web App Firebase Project
Adding Firebase to Your App
1) Creating a Project Folder
1) Create a folder on your computer where you want to save your Firebase projectfor example, Firebase-Project.
2) Open VS Code. Go to File > Open Folder and select the folder you've just created.
3) Go to Terminal > New Terminal. A new Terminal window should open on your project path.
2) Firebase Login
4) On the previous Terminal window, type the following:
firebase login
5) You'll be asked to collect CLI usage and error reporting information. Enter n and press Enter to deny.
6) After this, it will pop up a new window on your browser to login into your firebase account.
7) Allow Firebase CLI to access your google account.
8) After this, Firebase CLI login should be successful. You can close the browser window.
3) Initializing Web App Firebase Project
9) After successfully login in, run the following command to start a Firebase project directory in the current folder.
firebase init
10) You'll be asked if you want to initialize a Firebase project in the current directory. Enter Y and hit Enter.
11) Then, use up and down arrows and the Space key to select the options. Select the following options:
RealTime Database: Configure security rules file for Realtime Database and (optionally) provision default instance.
Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys
The selected options will show up with a green asterisk. Then, hit Enter.
12) Select the option Use an existing projectit should be highlighted in bluethen, hit Enter.
13) After that, select the Firebase project for this directoryit should be the project created in this previous tutorial. In my case, it is called esp32-firebase-demo. Then hit Enter.
14) Press Enter on the following question to select the default database security rules file: What file should be used for Realtime Database Security Rules?
15) Then, select the hosting options as shown below:
What do you want to use as your public directory? Hit Enter to select public.
Configure as a single-page app (rewrite urls to /index.html)? No
Set up automatic builds and deploys with GitHub? No
16) The Firebase project should now be initialized successfully. Notice that VS code created some essential files under your project folder.
The index.html file contains some HTML text to build a web page. For now, leave the default HTML text. The idea is to replace that with your own HTML text to build a custom web page for your needs. We'll do that later in this tutorial.
17) To check if everything went as expected, run the following command on the VS Code Terminal window.
firebase deploy
Now, you need to get your web app URL to access it.
4) Add Firebase To Your App
Leave VS Code opened. Meanwhile, you need to go to your Firebase account to add Firebase to your app.
18) Go to your Firebase console and select your project. Then, click on the web icon to add a web app to firebase (or the +Add app button).
19) Give your app a name. I simply called it test. Then, check the box next to Also set up Firebase Hosting for this App. Click Register app.
20) Then, copy the firebaseConfig object because you'll need it later.
Click Next on the proceeding steps.
After this, you can also access the firebaseConfig object if you go to your Project settings in your Firebase console.
21) Copy the authDomain. In my case, it is:
esp32-firebase-demo.firebaseapp.com
This is the URL that allows you to access your web app.
Paste the domain into your browser. You should see the following web page. This web page is built with the files placed in the public folder of your firebase project.
You can access that web page from anywhere in the world.
Congratulations, you've set up your Firebase App project correctly. Now, let's change the files in the public folder to show your own web page instead of that one.
Creating Firebase Web App
Now that you've created a Firebase project app successfully on VS Code, follow the next steps to customize the app to display the values saved on the Realtime Database.
index.html
Copy the following to your index.html file. This HTML file creates a simple web page that displays the readings saved on the Realtime Database created on this previous project.
<!-- Complete Project Details at: https://RandomNerdTutorials.com/ -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP Firebase App</title>
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"></script>
<!-- TODO: Add SDKs for Firebase products that you want to use
https://firebase.google.com/docs/web/setup#available-libraries -->
<script src="https://www.gstatic.com/firebasejs/8.8.1/firebase-database.js"></script>
<script>
// REPLACE WITH YOUR web app's Firebase configuration
var firebaseConfig = {
apiKey: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
authDomain: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
databaseURL: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
projectId: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
storageBucket: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
messagingSenderId: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
appId: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
var database = firebase.database();
</script>
<script src="app.js" defer></script>
</head>
<body>
<h1>ESP32 Firebase Web App Example</h2>
<p>Reading int: <span></span></p>
<p>Reading float: <span></span></p>
</body>
</html>
View raw code
You need to modify the code with your own firebaseConfig object (from step 20).
Let's take a quick look at the HTML file.
In the <head> of the HTML file, we must add all the required metadata.
The title of the web page is ESP Firebase App, but you can change it in the following line.
<title>ESP Firebase App</title>
You must add the following line to be able to use Firebase with your app.
<script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"></script>
You must also add any Firebase products you want to use. In this example, we're using the Realtime Database.
<script src="https://www.gstatic.com/firebasejs/8.8.1/firebase-database.js"></script>
Then, replace the firebaseConfig object with the one you've gotten from step 20.
var firebaseConfig = {
apiKey: "AIzaSyBPIfTiBcoWhVQ3ozmHH3JzgjQZAsCGuyk",
authDomain: "esp32-firebase-demo.firebaseapp.com",
databaseURL: "https://esp32-firebase-demo-default-rtdb.europe-west1.firebasedatabase.app",
projectId: "esp32-firebase-demo",
storageBucket: "esp32-firebase-demo.appspot.com",
messagingSenderId: "112982951680",
appId: "1:112982951680:web:f55c1f591739c33b8a15e5"
};
Finally, Firebase is initialized and we create a global variable called database that refers to our Firebase Realtime Database.
firebase.initializeApp(firebaseConfig);
var database = firebase.database();
We also reference the app.js file that we'll create next to include JavaScript functions that will allow us to update the HTML page with the database values.
<script src="app.js" defer></script>
We're done with the metadata. Now, let's go to the HTML parts that are visible to the usergo between the <body> and </body> tags.
We create a heading with the following text: ESP32 Firebase Web App Example, but you can change it to whatever you want.
<h1>ESP32 Firebase Web App Example</h2>
Then, we add two paragraphs to display the float and int values saved on the database. We create <span> tags with specific ids, so that we can refer to those HTML elements using JavaScript and insert the database values.
<p>Reading int: <span></span></p>
<p>Reading float: <span></span></p>
After making the necessary changes, you can save the HTML file.
app.js
Inside the public folder, create a file called app.js. You can do this on VS Code by selecting the public folder and then, clicking on the +file icon. This JavaScript file is responsible for updating the values on the web page any time there's a change on the database.
Copy the following to your app.js file.
// Complete Project Details at: https://RandomNerdTutorials.com/
// Database Paths
var dataFloatPath = 'test/float';
var dataIntPath = 'test/int';
// Get a database reference
const databaseFloat = database.ref(dataFloatPath);
const databaseInt = database.ref(dataIntPath);
// Variables to save database current values
var floatReading;
var intReading;
// Attach an asynchronous callback to read the data
databaseFloat.on('value', (snapshot) => {
floatReading = snapshot.val();
console.log(floatReading);
document.getElementById("reading-float").innerHTML = floatReading;
}, (errorObject) => {
console.log('The read failed: ' + errorObject.name);
});
databaseInt.on('value', (snapshot) => {
intReading = snapshot.val();
console.log(intReading);
document.getElementById("reading-int").innerHTML = intReading;
}, (errorObject) => {
console.log('The read failed: ' + errorObject.name);
});
View raw code
The following snippet is responsible for listening to changes on the test/float database path.
databaseFloat.on('value', (snapshot) => {
floatReading = snapshot.val();
console.log(floatReading);
document.getElementById("reading-float").innerHTML = floatReading;
}, (errorObject) => {
console.log('The read failed: ' + errorObject.name);
});
Whenever you insert a new value on that database path, we update the value on the HTML element with the reading-float id.
document.getElementById("reading-float").innerHTML = floatReading;
We follow a similar procedure to update the readings on the test/int database path.
databaseInt.on('value', (snapshot) => {
intReading = snapshot.val();
console.log(intReading);
document.getElementById("reading-int").innerHTML = intReading;
}, (errorObject) => {
console.log('The read failed: ' + errorObject.name);
});
Save the JavaScript file.
Deploy your App
After saving the HTML and JavaScript files, deploy your app on VS Code by running the following command.
firebase deploy
ESP32 Arduino Sketch
Upload the following code to your ESP32. This is the same code used in this previous project to write to the database. This code simply writes to the database every 15 seconds.
Don't forget to insert your network credentials, database URL, and Firebase Project API Key.
/*
Rui Santos
Complete project details at our blog.
- ESP32: https://RandomNerdTutorials.com/esp32-firebase-realtime-database/
- ESP8266: https://RandomNerdTutorials.com/esp8266-nodemcu-firebase-realtime-database/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Based in the RTDB Basic Example by Firebase-ESP-Client library by mobizt
https://github.com/mobizt/Firebase-ESP-Client/blob/main/examples/RTDB/Basic/Basic.ino
*/
#include <Arduino.h>
#if defined(ESP32)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#endif
#include <Firebase_ESP_Client.h>
//Provide the token generation process info.
#include "addons/TokenHelper.h"
//Provide the RTDB payload printing info and other helper functions.
#include "addons/RTDBHelper.h"
// Insert your network credentials
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// Insert Firebase project API Key
#define API_KEY "REPLACE_WITH_YOUR_FIREBASE_PROJECT_API_KEY"
// Insert RTDB URLefine the RTDB URL */
#define DATABASE_URL "REPLACE_WITH_YOUR_FIREBASE_DATABASE_URL"
//Define Firebase Data object
FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
unsigned long sendDataPrevMillis = 0;
int count = 0;
bool signupOK = false;
void setup(){
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED){
Serial.print(".");
delay(300);
}
Serial.println();
Serial.print("Connected with IP: ");
Serial.println(WiFi.localIP());
Serial.println();
/* Assign the api key (required) */
config.api_key = API_KEY;
/* Assign the RTDB URL (required) */
config.database_url = DATABASE_URL;
/* Sign up */
if (Firebase.signUp(&config, &auth, "", "")){
Serial.println("ok");
signupOK = true;
}
else{
Serial.printf("%s\n", config.signer.signupError.message.c_str());
}
/* Assign the callback function for the long running token generation task */
config.token_status_callback = tokenStatusCallback; //see addons/TokenHelper.h
Firebase.begin(&config, &auth);
Firebase.reconnectWiFi(true);
}
void loop(){
if (Firebase.ready() && signupOK && (millis() - sendDataPrevMillis > 15000 || sendDataPrevMillis == 0)){
sendDataPrevMillis = millis();
// Write an Int number on the database path test/int
if (Firebase.RTDB.setInt(&fbdo, "test/int", count)){
Serial.println("PASSED");
Serial.println("PATH: " + fbdo.dataPath());
Serial.println("TYPE: " + fbdo.dataType());
}
else {
Serial.println("FAILED");
Serial.println("REASON: " + fbdo.errorReason());
}
count++;
// Write an Float number on the database path test/float
if (Firebase.RTDB.setFloat(&fbdo, "test/float", 0.01 + random(0,100))){
Serial.println("PASSED");
Serial.println("PATH: " + fbdo.dataPath());
Serial.println("TYPE: " + fbdo.dataType());
}
else {
Serial.println("FAILED");
Serial.println("REASON: " + fbdo.errorReason());
}
}
}
View raw code
Demonstration
The ESP32 should be sending new readings every 15 seconds to the database.
Go to your App URL. You'll see the readings being updated every 15 seconds. The App updates the web page any time the ESP32 writes a new value.
In your Firebase Console, you can go to your project page, and check that new values are being written into the database every 15 seconds.
Congratulations! You've created a Firebase Web App to interface with the ESP32.
Wrapping Up
In this tutorial, you learned how to create a Firebase Web App to interface with the ESP32. You've learned how to use Firebase Hosting services and the Realtime Database.
We've built a simple example to get you started with Firebase. It simply displays some random numbers on a web page. The idea is to replace those numbers with sensor readings or GPIO states. Additionally, you may also add buttons, or sliders to the web page to control the ESP32 GPIOs. The possibilities are endless.
The example has some limitations but allows you to understand Firebase Web Apps potential for the ESP32. For example, at this point, anyone can write and read data from your database because we haven't set any database rules (it is in test mode). Additionally, we didn't protect it with any kind of authentication. Nonetheless, we hope you find this tutorial useful. And if you want to learn more you can also check the Firebase documentation.
If there is enough interest in this subject, we may create more Firebase tutorials with the ESP32. Let us know in the comments below if this is a subject that you are interested in.
We hope you find this tutorial useful.
We hope you find this tutorial useful. If you want to learn more about Firebase with the ESP32 and ESP8266 boards, check out our new eBook:
Firebase Web App with ESP32 and ESP8266
Learn more with the ESP32 with our resources:
Learn ESP32 with Arduino IDE (eBook + video course)
Build Web Servers with ESP32 and ESP8266 eBook (2nd Edition)
More ESP32 Projects and Tutorials
Getting Started with Firebase (Realtime Database)
This guide will get you started quickly with Firebase using the ESP32 board. Firebase is Google's mobile application development platform that includes many services to manage data from IOS, Android, or web applications. You'll create a Firebase project with a realtime database (RTDB), and you'll learn how to store and read values from the database with your ESP32.
In a later tutorial, you'll learn how to create a Firebase web app that you can access from anywhere to monitor and control your ESP32 using firebase's realtime database:
ESP32 with Firebase Creating a Web App
We have a similar tutorial for the ESP8266 board: Getting Started with Firebase (Realtime Database)
What is Firebase?
Firebase is Google's mobile application development platform that helps you build, improve, and grow your app. It has many services used to manage data from any android, IOS, or web application.
The following paragraph clearly explains the advantages of using Firebase:
Firebase is a toolset to build, improve, and grow your app, and the tools it gives you cover a large portion of the services that developers would normally have to build themselves but don't really want to build because they'd rather be focusing on the app experience itself. This includes things like analytics, authentication, databases, configuration, file storage, push messaging, and the list goes on. The services are hosted in the cloud and scale with little to no effort on the part of the developer.
This paragraph was taken from this article, and we recommend that you read that article if you want to understand better what firebase is and what it allows you to do.
You can use the ESP32 to connect and interact with your Firebase project, and you can create applications to control the ESP32 via Firebase from anywhere in the world.
In this tutorial, we'll create a Firebase project with a realtime database, and we'll use the ESP32 to store and read data from the database. The ESP32 can interact with the database from anywhere in the world as long as it is connected to the internet.
This means that you can have two ESP32 boards in different networks, with one board storing data and the other board reading the most recent data, for example.
In a later tutorial, we'll create a web app using Firebase that will control the ESP32 to display sensor readings or control outputs from anywhere in the world.
Project Overview
In this tutorial, you'll learn how to create a Firebase project with a realtime database and store and read data from the database using the ESP32.
To follow this project, first, you need to set up a Firebase project and create a realtime database for that project. Then, you'll program the ESP32 to store and read data from the database. This tutorial is divided into three sections.
Create a Firebase Project
ESP32: Store data to the Firebase Realtime Database
ESP32: Read data from the Firebase Realtime Database
Let's get started!
Set Up a Firebase Account and Create a New Project
1.Create a New Project
Follow the next instructions to create a new project on Firebase.
Go to Firebase and sign in using a Google Account.
Click Get Started, and then Add project to create a new project.
Give a name to your project, for example: ESP32 Firebase Demo.
Disable the option Enable Google Analytics for this project as it is not needed and click Create project.
It will take a few seconds setting up your project. Then, click Continue when it's ready.
You'll be redirected to your Project console page.
2. Set Authentication Methods
You need to set authentication methods for your app.
Most apps need to know the identity of a user. In other words, it takes care of logging in and identify the users (in this case, the ESP32). Knowing a user's identity allows an app to securely save user data in the cloud and provide the same personalized experience across all of the user's devices. To learn more about the authentication methods, you can read the documentation.
On the left sidebar, click on Authentication and then on Get started.
There are several authentication methods like email and password, Google Account, Facebook account, and others.
For testing purposes, we can select the Anonymous user (require authentication without requiring users to sign in first by creating temporary anonymous accounts). Enable that option and click Save.
3. Creating a Realtime Database
The next step is creating a Realtime Database for your project. Follow the next steps to create the database.
On the left sidebar click on Realtime Database and then, click on Create Database.
Select your database location. It should be the closest to your location.
Set up security rules for your database. For testing purposes, select Start in test mode. In later tutorials you'll learn how to secure your database using database rules.
Your database is now created. You need to copy and save the database URLhighlighted in the following imagebecause you'll need it later in your ESP32 code.
The Realtime Database is all set. Now, you also need to get your project API key.
4. Get Project API Key
To get your project's API key, on the left sidebar click on Project Settings.
Copy the API Key to a safe place because you'll need it later.
Now, you have everything ready to interface the ESP32 with the database.
Program the ESP32 to Interface with Firebase
Now that the Firebase Realtime Database is created, you'll learn how to interface the ESP32 with the database.
To program the ESP32, you can use Arduino IDE, VS Code with the PlatformIO extension, or other suitable software.
Note: for firebase projects, we recommend using VS Code with the PlatformIO extension because if you want to develop a web application to make the bridge between the ESP32 and Firebase, VS Code provides all the tools to do that. However, we won't build the web application in this tutorial, so you can use Arduino IDE.
Install the Firebase-ESP-Client Library
There is a library with lots of examples to use Firebase with the ESP32: the Firebase-ESP-Client library. This library is compatible with both the ESP32 and ESP8266 boards.
In this tutorial, we'll look at simple examples to store and read data from the database. The library provides many other examples that you can check here. It also provides detailed documentation explaining how to use the library.
Installation VS Code + PlatformIO
If you're using VS Code with the PlatformIO extension, click on the PIO Home icon and then select the Libraries tab. Search for Firebase ESP Client. Select the Firebase Arduino Client Library for ESP8266 and ESP32.
Then, click Add to Project and select the project you're working on.
Also, change the monitor speed to 115200 by adding the following line to the platformio.ini file of your project:
monitor_speed = 115200
Installation Arduino IDE
If you're using Arduino IDE, follow the next steps to install the library.
Go to Sketch > Include Library > Manage Libraries
Search for Firebase ESP Client and install the Firebase Arduino Client Library for ESP8266 and ESP32 by Mobitz.
Note: We are using version 2.3.7. If you have issues compiling your code with more recent versions of the library, downgrade to version 2.3.7.
Now, you're all set to start programming the ESP32 board to interact with the database.
ESP32 Store Data to Firebase Database
Copy the following code to your Arduino IDE. This sketch inserts an int and a float number into the database every 15 seconds. This is a simple example showing you how to connect the ESP32 to the database and store data. This is also compatible with ESP8266 boards.
Note: We are using version 2.3.7 of the Firebase ESP Client library. If you have issues compiling your code with more recent versions of the library, downgrade to version 2.3.7.
/*
Rui Santos
Complete project details at our blog.
- ESP32: https://RandomNerdTutorials.com/esp32-firebase-realtime-database/
- ESP8266: https://RandomNerdTutorials.com/esp8266-nodemcu-firebase-realtime-database/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Based in the RTDB Basic Example by Firebase-ESP-Client library by mobizt
https://github.com/mobizt/Firebase-ESP-Client/blob/main/examples/RTDB/Basic/Basic.ino
*/
#include <Arduino.h>
#if defined(ESP32)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#endif
#include <Firebase_ESP_Client.h>
//Provide the token generation process info.
#include "addons/TokenHelper.h"
//Provide the RTDB payload printing info and other helper functions.
#include "addons/RTDBHelper.h"
// Insert your network credentials
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// Insert Firebase project API Key
#define API_KEY "REPLACE_WITH_YOUR_FIREBASE_PROJECT_API_KEY"
// Insert RTDB URLefine the RTDB URL */
#define DATABASE_URL "REPLACE_WITH_YOUR_FIREBASE_DATABASE_URL"
//Define Firebase Data object
FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
unsigned long sendDataPrevMillis = 0;
int count = 0;
bool signupOK = false;
void setup(){
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED){
Serial.print(".");
delay(300);
}
Serial.println();
Serial.print("Connected with IP: ");
Serial.println(WiFi.localIP());
Serial.println();
/* Assign the api key (required) */
config.api_key = API_KEY;
/* Assign the RTDB URL (required) */
config.database_url = DATABASE_URL;
/* Sign up */
if (Firebase.signUp(&config, &auth, "", "")){
Serial.println("ok");
signupOK = true;
}
else{
Serial.printf("%s\n", config.signer.signupError.message.c_str());
}
/* Assign the callback function for the long running token generation task */
config.token_status_callback = tokenStatusCallback; //see addons/TokenHelper.h
Firebase.begin(&config, &auth);
Firebase.reconnectWiFi(true);
}
void loop(){
if (Firebase.ready() && signupOK && (millis() - sendDataPrevMillis > 15000 || sendDataPrevMillis == 0)){
sendDataPrevMillis = millis();
// Write an Int number on the database path test/int
if (Firebase.RTDB.setInt(&fbdo, "test/int", count)){
Serial.println("PASSED");
Serial.println("PATH: " + fbdo.dataPath());
Serial.println("TYPE: " + fbdo.dataType());
}
else {
Serial.println("FAILED");
Serial.println("REASON: " + fbdo.errorReason());
}
count++;
// Write an Float number on the database path test/float
if (Firebase.RTDB.setFloat(&fbdo, "test/float", 0.01 + random(0,100))){
Serial.println("PASSED");
Serial.println("PATH: " + fbdo.dataPath());
Serial.println("TYPE: " + fbdo.dataType());
}
else {
Serial.println("FAILED");
Serial.println("REASON: " + fbdo.errorReason());
}
}
}
View raw code
You need to insert your network credentials, URL database, and project API key for the project to work.
This sketch was based on the basic example provided by the library. You can find more examples here.
How the Code Works
Continue reading to learn how the code works, or skip to the demonstration section.
First, include the required libraries. The WiFi.h library to connect the ESP32 to the internet (or the ESP8266WiFi.h in case of the ESP8266 board) and the Firebase_ESP_Client.h library to interface the boards with Firebase.
#include <Arduino.h>
#if defined(ESP32)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#endif
#include <Firebase_ESP_Client.h>
You also need to include the following for the Firebase library to work.
//Provide the token generation process info.
#include "addons/TokenHelper.h"
//Provide the RTDB payload printing info and other helper functions.
#include "addons/RTDBHelper.h"
Include your network credentials in the following lines.
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
Insert your firebase project API keythe one you've gotten in section 4.1.
#define API_KEY "REPLACE_WITH_YOUR_FIREBASE_PROJECT_API_KEY"
Insert your database URLsee section 3.4.
#define DATABASE_URL "REPLACE_WITH_YOUR_FIREBASE_DATABASE_URL"
setup()
In the setup(), connect your board to your network.
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED){
Serial.print(".");
delay(300);
}
Serial.println();
Serial.print("Connected with IP: ");
Serial.println(WiFi.localIP());
Serial.println();
Assign the API key and the database URL to the Firebase configuration.
/* Assign the api key (required) */
config.api_key = API_KEY;
/* Assign the RTDB URL (required) */
config.database_url = DATABASE_URL;
The following lines take care of the signup for an anonymous user. Notice that you use the signUp() method, and the last two arguments are empty (anonymous user).
/* Sign up */
if (Firebase.signUp(&config, &auth, "", "")){
Serial.println("ok");
signupOK = true;
}
else{
Serial.printf("%s\n", config.signer.signupError.message.c_str());
}
Note: in the anonymous user signup, every time the ESP connects, it creates a new anonymous user.
If the sign-in is successful, the signupOK variable changes to true.
signupOK = true;
The library provides examples for other authentication methods like signing in as a user with email and password, using the database legacy auth token, etc. You can check all the examples for other authentication methods here. If you end up using other authentication methods, don't forget that you need to enable them on your firebase project (Build > Authentication > Sign-in method).
loop()
In the loop(), we'll send data to the database periodically (if the signup is successful and everything is set up).
if (Firebase.ready() && signupOK && (millis() - sendDataPrevMillis > 15000 || sendDataPrevMillis == 0)){
Send Data to the Database
As mentioned in the library documentation, to store data at a specific node in the Firebase RTDB (realtime database), use the following functions: set, setInt, setFloat, setDouble, setString, setJSON, setArray, setBlob, and setFile.
These functions return a boolean value indicating the success of the operation, which will be true if all of the following conditions are met:
Server returns HTTP status code 200.
The data types matched between request and response.Only setBlob and setFile functions that make a silent request to Firebase server, thus no payload response returned.
In our example, we'll send an integer number, so we need to use the setInt() function as follows:
Firebase.RTDB.setInt(&fbdo, "test/int", count)
The second argument is the database node path, and the last argument is the value you want to pass to that database pathyou can choose any other database path. In this case, we're passing the value saved in the count variable.
Here's the complete snippet that stores the value in the database and prints a success or failed message.
if (Firebase.RTDB.setInt(&fbdo, "test/int", count)) {
Serial.println("PASSED");
Serial.println("PATH: " + fbdo.dataPath());
Serial.println("TYPE: " + fbdo.dataType());
}
else {
Serial.println("FAILED");
Serial.println("REASON: " + fbdo.errorReason());
}
We proceed in a similar way to store a float value. We're storing a random float value on the test/float path.
// Write an Float number on the database path test/float
if (Firebase.RTDB.setFloat(&fbdo, "test/float", 0.01 + random(0, 100))) {
Serial.println("PASSED");
Serial.println("PATH: " + fbdo.dataPath());
Serial.println("TYPE: " + fbdo.dataType());
}
else {
Serial.println("FAILED");
Serial.println("REASON: " + fbdo.errorReason());
}
Demonstration
Upload the code to your ESP32 board. Don't forget to insert your network credentials, database URL path, and the project API key.
After uploading the code, open the Serial Monitor at a baud rate of 115200 and press the ESP32 on-board reset button so it starts running the code.
If everything works as expected, the values should be stored in the database, and you should get success messages.
Go to your project's Firebase Realtime database, and you'll see the values saved on the different node paths. Every 15 seconds, it saves a new value. The database blinks when new values are saved.
Congratulations! You've successfully stored data in Firebase's realtime database using the ESP32. In the next section, you'll learn to read values from the different database's node paths.
ESP32 Read From Firebase Database
In this section, you'll learn how to read data from the database. We'll read the data stored in the previous section. Remember that we saved an int value in the test/int path and a float value in the test/float path.
The following example reads the values stored in the database. Upload the following code to your board. You can use the same ESP32 board or another board to get the data posted by the previous ESP32.
Note: We are using version 2.3.7 of the Firebase ESP Client library. If you have issues compiling your code with more recent versions of the library, downgrade to version 2.3.7.
/*
Rui Santos
Complete project details at our blog.
- ESP32: https://RandomNerdTutorials.com/esp32-firebase-realtime-database/
- ESP8266: https://RandomNerdTutorials.com/esp8266-nodemcu-firebase-realtime-database/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Based in the RTDB Basic Example by Firebase-ESP-Client library by mobizt
https://github.com/mobizt/Firebase-ESP-Client/blob/main/examples/RTDB/Basic/Basic.ino
*/
#include <Arduino.h>
#if defined(ESP32)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#endif
#include <Firebase_ESP_Client.h>
//Provide the token generation process info.
#include "addons/TokenHelper.h"
//Provide the RTDB payload printing info and other helper functions.
#include "addons/RTDBHelper.h"
// Insert your network credentials
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// Insert Firebase project API Key
#define API_KEY "REPLACE_WITH_YOUR_FIREBASE_PROJECT_API_KEY"
// Insert RTDB URLefine the RTDB URL */
#define DATABASE_URL "REPLACE_WITH_YOUR_FIREBASE_DATABASE_URL"
//Define Firebase Data object
FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
unsigned long sendDataPrevMillis = 0;
int intValue;
float floatValue;
bool signupOK = false;
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(300);
}
Serial.println();
Serial.print("Connected with IP: ");
Serial.println(WiFi.localIP());
Serial.println();
/* Assign the api key (required) */
config.api_key = API_KEY;
/* Assign the RTDB URL (required) */
config.database_url = DATABASE_URL;
/* Sign up */
if (Firebase.signUp(&config, &auth, "", "")) {
Serial.println("ok");
signupOK = true;
}
else {
Serial.printf("%s\n", config.signer.signupError.message.c_str());
}
/* Assign the callback function for the long running token generation task */
config.token_status_callback = tokenStatusCallback; //see addons/TokenHelper.h
Firebase.begin(&config, &auth);
Firebase.reconnectWiFi(true);
}
void loop() {
if (Firebase.ready() && signupOK && (millis() - sendDataPrevMillis > 15000 || sendDataPrevMillis == 0)) {
sendDataPrevMillis = millis();
if (Firebase.RTDB.getInt(&fbdo, "/test/int")) {
if (fbdo.dataType() == "int") {
intValue = fbdo.intData();
Serial.println(intValue);
}
}
else {
Serial.println(fbdo.errorReason());
}
if (Firebase.RTDB.getFloat(&fbdo, "/test/float")) {
if (fbdo.dataType() == "float") {
floatValue = fbdo.floatData();
Serial.println(floatValue);
}
}
else {
Serial.println(fbdo.errorReason());
}
}
}
View raw code
Don't forget to insert your network credentials, database URL, and API key.
How the Code Works
The code is very similar to the previous section's example, but it reads data from the database. Let's take a look at the relevant parts for this section.
Data at a specific node in Firebase RTDB can be read through the following functions: get, getInt, getFloat, getDouble, getBool, getString, getJSON, getArray, getBlob, getFile.
These functions return a boolean value indicating the success of the operation, which will be true if all of the following conditions were met:
Server returns HTTP status code 200
The data types matched between request and response.
The database data's payload (response) can be read or access through the following Firebase Data object's functions: fbdo.intData, fbdo.floatData, fbdo.doubleData, fbdo.boolData, fbdo.stringData, fbdo.jsonString, fbdo.jsonObject, fbdo.jsonObjectPtr, fbdo.jsonArray, fbdo.jsonArrayPtr, fbdo.jsonData (for keeping parse/get result), and fbdo.blobData.
If you use a function that doesn't match the returned data type in the database, it will return empty (string, object, or array).
The data type of the returning payload can be determined by fbdo.getDataType.
The following snippet shows how to get an integer value stored in the test/int node. First, we use the getInt() function; then, we check if the data type is an integer with fbdo.dataType(), and finally, the fdbo.intData() gets the value stored in that node.
if (Firebase.RTDB.getInt(&fbdo, "/test/int")) {
if (fbdo.dataType() == "int") {
intValue = fbdo.intData();
Serial.println(intValue);
}
}
else {
Serial.println(fbdo.errorReason());
}
We use a similar snippet to get the float value.
if (Firebase.RTDB.getFloat(&fbdo, "/test/float")) {
if (fbdo.dataType() == "float") {
floatValue = fbdo.floatData();
Serial.println(floatValue);
}
}
else {
Serial.println(fbdo.errorReason());
}
Demonstration
Upload the code to your board. Then, open the Serial Monitor at a baud rate of 115200. After a few seconds, it will print the values saved on the database.
Wrapping Up
Congratulations! In this tutorial, you've created a Firebase project with a Realtime Database and learned how to store and read data from the database using the ESP32.
To keep things simple, we've stored sample values on the database. The idea is to save useful data like sensor readings or GPIO states.
Then, you can access the database with another ESP32 to get the data or create a Firebase web app to use that data to display sensor readings or control the ESP32 GPIOs from anywhere in the world. We cover the basics of how to create a Firebase Web App in this tutorial.
We hope you find this tutorial useful. If you want to learn more about Firebase with the ESP32 and ESP8266 boards, check out our new eBook:
Firebase Web App with ESP32 and ESP8266
Guide for TCA9548A I2C Multiplexer: ESP32, ESP8266, Arduino
In this guide, you'll learn how to expand the I2C bus ports (ESP32, ESP8266, Arduino) using the TCA9458A 1-to-8 I2C Multiplexer. This piece of hardware is useful if you want to control multiple I2C devices with the same I2C address. For example, multiple OLED displays, or multiple sensors like the BME280.
This tutorial is compatible with ESP32, ESP8266, and Arduino boards. We'll program the boards using Arduino IDE.
Table of Contents
In this tutorial, we'll cover the following topics:
Introducing the TCA9548A I2C Multiplexer
TCA9548A Multiplexer Features
TCA9548A Multiplexer I2C address
TCA9548A Pinout
TCA9548A Multiplexer Selecting an I2C Bus
Example 1: Connecting multiple OLED displays
Example 2: Connecting multiple I2C sensors (BME280)
Introducing the TCA9548A 1-to-8 I2C Multiplexer
The I2C communication protocol allows you to communicate with multiple I2C devices on the same I2C bus as long as all devices have a unique I2C address. However, it will not work if you want to connect multiple I2C devices with the same address.
The TCA9548A I2C multiplexer allows you to communicate with up to 8 I2C devices with the same I2C bus. The multiplexer communicates with a microcontroller using the I2C communication protocol. Then, you can select which I2C bus on the multiplexer you want to address.
To address a specific port, you just need to send a single byte to the multiplexer with the desired output port number.
TCA9548A Multiplexer Features
Here's a summary of its main features:
1 to 8 bidireccional translating switches
Active-low reset input
Three address pinsup to 8 TCA9548A devices on the same I2C bus
Channel selection through an I2C bus
Operating power supply voltage range: 1.65V to 5.5V
5V tolerant pins
For a more detailed description, consult the datasheet.
TCA9548A Multiplexer I2C Address
The TCA9548A Multiplexer communicates with a microcontroller using the I2C communication protocol. So, it needs an I2C address. The address of the multiplexer is configurable. You can select a value from 0x70 to 0x77 by adjusting the values of the A0, A1, and A2 pins, as shown in the table below.
A0 |
A1 |
A2 |
I2C Address |
LOW |
LOW |
LOW |
0x70 |
HIGH |
LOW |
LOW |
0x71 |
LOW |
HIGH |
LOW |
0x72 |
HIGH |
HIGH |
LOW |
0x73 |
LOW |
LOW |
HIGH |
0x74 |
HIGH |
LOW |
HIGH |
0x75 |
LOW |
HIGH |
HIGH |
0x76 |
HIGH |
HIGH |
HIGH |
0x77 |
So, you can connect up to 8 TCA9548A multiplexers to the same I2C bus, which would allow you to connect 64 devices with the same address using only one I2C bus of the microcontroller.
For example, if you connect A0, A1, and A2 to GND, it sets address 0x70 for the multiplexer.
TCA9548A Pinout
The following table describes the TCA9584A Pinout.
Pin |
Description |
VIN |
Powers the multiplexer |
GND |
Connect to GND |
SDA |
Connect to the master microcontroller SDA pin |
SCL |
Connect to the master microcontroller SCL pin |
RST |
Active low RST pincan be used to reset the multiplexer |
A0 |
Selects multiplexer I2C addressconnect to GND or VCC |
A1 |
Selects multiplexer I2C addressconnect to GND or VCC |
A2 |
Selects multiplexer I2C addressconnect to GND or VCC |
SD0 |
SDA for channel 0 |
SC0 |
SCL for channel 0 |
SD1 |
SDA for channel 1 |
SC1 |
SCL for channel 1 |
SD2 |
SDA for channel 2 |
SC2 |
SCL for channel 2 |
SD3 |
SDA for channel 3 |
SC3 |
SCL for channel 3 |
SD4 |
SDA for channel 4 |
SC4 |
SCL for channel 4 |
SD5 |
SDA for channel 5 |
SC5 |
SCL for channel 5 |
SD6 |
SDA for channel 6 |
SC6 |
SCL for channel 6 |
SD7 |
SDA for channel 7 |
SC7 |
SCL for channel 7 |
TCA9548A I2C Multiplexer Selecting an I2C Bus
As mentioned previously, to select a specific I2C bus to read/write data, you just need to send a single byte to the multiplexer with the desired output port number (0 to 7).
To do that, you can simply use the following user-defined function:
void TCA9548A(uint8_t bus){
Wire.beginTransmission(0x70); // TCA9548A address is 0x70
Wire.write(1 << bus); // send byte to select bus
Wire.endTransmission();
Serial.print(bus);
}
Then, you just need to call that function and pass as an argument the port bus number you want to control before sending the I2C commands. For example, to control the device connected to bus number 3, you would call the following line before calling other I2C commands (note that it starts at 0):
TCA9548A(2);
You'll see how this works with practical examples in the following sections.
Control Multiple OLED DisplaysTCA9548A I2C Multiplexer
In this section, we'll show you how to control multiple OLED displays. As an example, we'll control four OLED displays, but you can connect up to 8 displays.
Parts Required
Here's a list of the parts required for this example:
Microcontroller (ESP32, ESP8266, Arduino, or other);
TCA9548A I2C Multiplexer
Multiple OLED Displays (up to 8)
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Multiple OLED Displays with I2C Multiplexer Circuit
Connect four OLED displays as shown in the following schematic diagram. We're using buses number 2, 3, 4 and 5. You can choose any other port number.
We're also connecting A0, A1, and A2 to GND. This selects address 0x70 for the multiplexer.
Here are the default I2C pins depending on the microcontroller you're using:
Microcontroller |
I2C Pins |
ESP32 |
GPIO 22 (SCL), GPIO 21 (SDA) |
ESP8266 |
GPIO 5 (D1) (SCL), GPIO 4 (D2) (SDA) |
Arduino Uno |
A5 (SCL), A4 (SDA) |
Multiple OLED Displays with I2C Multiplexer Code
Controlling the displays is as easy as controlling one display. You just need to consider selecting the right I2C bus before sending the commands to write to the display.
To learn how to control an I2C display, you can read the following articles:
ESP32 OLED Display with Arduino IDE
ESP8266 0.96 inch OLED Display with Arduino IDE
Guide for I2C OLED Display with Arduino
Installing Libraries
We'll use the following libraries to control the OLED display. Make sure you have these libraries installed:
Adafruit_SSD1306 library
Adafruit_GFX library
You can install the libraries using the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the library name.
If you're using VS Code with the PlatformIO extension, copy the following to the platformio.ini file to include the libraries.
lib_deps = adafruit/Adafruit SSD1306@^2.4.6
adafruit/Adafruit GFX Library@^1.10.10
After installing the libraries, you can proceed.
Copy the following code to your Arduino IDE and upload it to your board. It will work straight away.
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/tca9548a-i2c-multiplexer-esp32-esp8266-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Select I2C BUS
void TCA9548A(uint8_t bus){
Wire.beginTransmission(0x70); // TCA9548A address
Wire.write(1 << bus); // send byte to select bus
Wire.endTransmission();
Serial.print(bus);
}
void setup() {
Serial.begin(115200);
// Start I2C communication with the Multiplexer
Wire.begin();
// Init OLED display on bus number 2
TCA9548A(2);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// Clear the buffer
display.clearDisplay();
// Init OLED display on bus number 3
TCA9548A(3);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// Clear the buffer
display.clearDisplay();
// Init OLED display on bus number 4
TCA9548A(4);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// Clear the buffer
display.clearDisplay();
// Init OLED display on bus number 5
TCA9548A(5);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// Clear the buffer
display.clearDisplay();
// Write to OLED on bus number 2
TCA9548A(2);
display.clearDisplay();
display.setTextSize(8);
display.setTextColor(WHITE);
display.setCursor(45, 10);
// Display static text
display.println("1");
display.display();
// Write to OLED on bus number 3
TCA9548A(3);
display.clearDisplay();
display.setTextSize(8);
display.setTextColor(WHITE);
display.setCursor(45, 10);
// Display static text
display.println("2");
display.display();
// Write to OLED on bus number 4
TCA9548A(4);
display.clearDisplay();
display.setTextSize(8);
display.setTextColor(WHITE);
display.setCursor(45, 10);
// Display static text
display.println("3");
display.display();
// Write to OLED on bus number 5
TCA9548A(5);
display.clearDisplay();
display.setTextSize(8);
display.setTextColor(WHITE);
display.setCursor(45, 10);
// Display static text
display.println("4");
display.display();
}
void loop() {
}
View raw code
How the Code Works
Continue reading to learn how the code works or skip to the Demonstration section.
First, import the required libraries to control the OLED display: Adafruit_GFX and Adafruit_SSD1306. The Wire library is needed to use the I2C communication protocol.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Define the OLED width and height.
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Create an Adafruit_SSD1306 instance to communicate with the OLED display.
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
You can use the same instance to communicate with all displays. In that case, you need to clear the display buffer (display.clearDisplay()) before writing to another OLED.
Alternatively, you can create multiple Adafruit_SSD1306 instances, one for each OLED. In that case, you don't need to clear the buffer. We'll show you an example with multiple instances at the end of this section.
Select the I2C Channel
The TCA9548A() function can be called to select the bus that you want to communicate with. It sends a byte to the multiplexer with the port number.
// Select I2C BUS
void TCA9548A(uint8_t bus){
Wire.beginTransmission(0x70); // TCA9548A address
Wire.write(1 << bus); // send byte to select bus
Wire.endTransmission();
Serial.print(bus);
}
You must call this function whenever you want to select the I2C port.
setup()
In the setup(), initialize a serial communication for debugging purposes.
Serial.begin(115200);
Start I2C communication on the default I2C pins with the I2C multiplexer.
Wire.begin();
Then, initialize each display. The following lines show an example for the first OLED display (it is connected to bus number 2).
//Init OLED display on bus number 2
TCA9548A(2);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// Clear the buffer
display.clearDisplay();
Initializing the other displays is similar, but you need to call the TCA9548A() function with the corresponding I2C bus number.
Then, write something to the displays. Don't forget you need to call the TCA9548A() function every time you want to switch between OLEDs. You also need to clear the display buffer before writing anything to the OLED.
// Write to OLED on bus number 2
TCA9548A(2);
display.clearDisplay();
display.setTextSize(8);
display.setTextColor(WHITE);
display.setCursor(45, 10);
// Display static text
display.println("1");
display.display();
In this case, we're just printing a different number on each display. Here's an example for OLED number 4 (it is connected to bus number 5).
// Write to OLED on bus number 5
TCA9548A(5);
display.clearDisplay();
display.setTextSize(8);
display.setTextColor(WHITE);
display.setCursor(45, 10);
// Display static text
display.println("4");
display.display();
And that's pretty much how the code works.
The following code shows a similar example but using multiple Adafruit_SSD1306 instances. Notice that you don't need to clear the buffer before writing to the display.
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/tca9548a-i2c-multiplexer-esp32-esp8266-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display1(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Adafruit_SSD1306 display2(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Adafruit_SSD1306 display3(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Adafruit_SSD1306 display4(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Select I2C BUS
void TCA9548A(uint8_t bus){
Wire.beginTransmission(0x70); // TCA9548A address
Wire.write(1 << bus); // send byte to select bus
Wire.endTransmission();
Serial.print(bus);
}
void setup() {
Serial.begin(115200);
// Start I2C communication with the Multiplexer
Wire.begin();
// Init OLED display on bus number 2 (display 1)
TCA9548A(2);
if(!display1.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// Clear the buffer
display1.clearDisplay();
// Init OLED display on bus number 3
TCA9548A(3);
if(!display2.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// Clear the buffer
display2.clearDisplay();
// Init OLED display on bus number 4
TCA9548A(4);
if(!display3.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// Clear the buffer
display3.clearDisplay();
// Init OLED display on bus number 5
TCA9548A(5);
if(!display4.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
// Clear the buffer
display4.clearDisplay();
// Write to OLED on bus number 2
TCA9548A(2);
display1.setTextSize(8);
display1.setTextColor(WHITE);
display1.setCursor(45, 10);
// Display static text
display1.println("1");
display1.display();
// Write to OLED on bus number 3
TCA9548A(3);
display2.setTextSize(8);
display2.setTextColor(WHITE);
display2.setCursor(45, 10);
// Display static text
display2.println("2");
display2.display();
// Write to OLED on bus number 4
TCA9548A(4);
display3.setTextSize(8);
display3.setTextColor(WHITE);
display3.setCursor(45, 10);
// Display static text
display3.println("3");
display3.display();
// Write to OLED on bus number 5
TCA9548A(5);
display4.setTextSize(8);
display4.setTextColor(WHITE);
display4.setCursor(45, 10);
// Display static text
display4.println("4");
display4.display();
}
void loop() {
}
View raw code
Demonstration
Upload the code to your board. Here's what you should get.
As you can see, it is pretty easy to control multiple OLED displays showing different graphics using an I2C multiplexer.
Read Multiple BME280 Sensors TCA9548A I2C Multiplexer
In this section, you'll learn how to read data from multiple BME280 sensors using the TCA9548A I2C multiplexer. We'll read data from four sensors, but you can hook up to 8 sensors.
Parts Required
Here's a list of the parts required for this example:
Microcontroller (ESP32, ESP8266, Arduino, or other);
TCA9548A I2C Multiplexer
Multiple BME280 sensors (up to 8)
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Multiple BME280 Sensors with I2C Multiplexer Circuit
Connect four BME280 sensors as shown in the following schematic diagram. We're using buses number 2, 3, 4 and 5. You can choose any other port numbers.
We're also connecting A0, A1, and A2 to GND. This selects address 0x70 for the multiplexer.
Here are the default I2C pins depending on the microcontroller you're using:
Microcontroller |
I2C Pins |
ESP32 |
GPIO 22 (SCL), GPIO 21 (SDA) |
ESP8266 |
GPIO 5 (D1) (SCL), GPIO 4 (D2) (SDA) |
Arduino Uno |
A5 (SCL), A4 (SDA) |
Multiple BME280 Sensors with I2C Multiplexer Code
Similar to the OLED display, reading data from multiple sensors is as easy as controlling one single sensor. You just need to take into account selecting the right I2C bus before communicating with the sensor.
To learn how to read data from a BME280 sensor:
ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity)
ESP8266 with BME280 using Arduino IDE (Pressure, Temperature, Humidity)
Guide for BME280 Sensor with Arduino (Pressure, Temperature, Humidity)
Installing Libraries
We'll use the following libraries to read from the BME280 sensor. Make sure you have these libraries installed:
Adafruit_BME280 library
Adafruit_Sensor library
You can install the libraries using the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the library name.
If you're using VS Code with the PlatformIO extension, copy the following to the platformio.ini file to include the libraries.
lib_deps = adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/Adafruit BME280 Library @ ^2.1.2
After installing the libraries, you can proceed.
Copy the following code to your Arduino IDE and upload it to your board. It will work straight away.
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/tca9548a-i2c-multiplexer-esp32-esp8266-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define SEALEVELPRESSURE_HPA (1022)
Adafruit_BME280 bme1; // I2C
Adafruit_BME280 bme2; // I2C
Adafruit_BME280 bme3; // I2C
Adafruit_BME280 bme4; // I2C
// Select I2C BUS
void TCA9548A(uint8_t bus){
Wire.beginTransmission(0x70); // TCA9548A address
Wire.write(1 << bus); // send byte to select bus
Wire.endTransmission();
}
void printValues(Adafruit_BME280 bme, int bus) {
TCA9548A (bus);
Serial.print("Sensor number on bus");
Serial.println(bus);
Serial.print("Temperature = ");
Serial.print(bme.readTemperature());
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bme.readPressure() / 100.0F);
Serial.println(" hPa");
Serial.print("Approx. Altitude = ");
Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(" m");
Serial.print("Humidity = ");
Serial.print(bme.readHumidity());
Serial.println(" %");
Serial.println();
}
void setup() {
Serial.begin(115200);
// Start I2C communication with the Multiplexer
Wire.begin();
// Init sensor on bus number 2
TCA9548A(2);
if (!bme1.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor on bus 2, check wiring!");
while (1);
}
Serial.println();
// Init sensor on bus number 3
TCA9548A(3);
if (!bme2.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor on bus 2, check wiring!");
while (1);
}
Serial.println();
// Init sensor on bus number 4
TCA9548A(4);
if (!bme3.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor on bus 2, check wiring!");
while (1);
}
Serial.println();
// Init sensor on bus number 5
TCA9548A(5);
if (!bme4.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor on bus 2, check wiring!");
while (1);
}
Serial.println();
}
void loop() {
//Print values for sensor 1
printValues(bme1, 2);
printValues(bme2, 3);
printValues(bme3, 4);
printValues(bme4, 5);
delay(5000);
}
View raw code
How the Code Works
Continue reading to learn how the code works or skip to the Demonstration section.
First, import the required libraries to control the BME280 display: Adafruit_BME280 and Adafruit_Sensor. The Wire library is needed to use the I2C communication protocol.
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
Then, you need to create several instances of Adafruit_BME280, one for each sensor: bme1, bme2, bme3, and bme4.
Adafruit_BME280 bme1; // I2C
Adafruit_BME280 bme2; // I2C
Adafruit_BME280 bme3; // I2C
Adafruit_BME280 bme4; // I2C
Select the I2C Channel
The TCA9548A() function can be called to select the bus that you want to communicate with. It sends a byte to the multiplexer with the port number.
// Select I2C BUS
void TCA9548A(uint8_t bus){
Wire.beginTransmission(0x70); // TCA9548A address
Wire.write(1 << bus); // send byte to select bus
Wire.endTransmission();
Serial.print(bus);
}
You must call this function whenever you want to select the I2C port.
printValues() function
Then, we create a function printValues() that allows us to print in the Serial Monitor the values for each sensor. This function allows us to pass the Adafruit_BME280 instance and its bus.
Inside the function, we select the I2C bus we want to talk to by calling the TCA9548A() function and passing the bus as an argument.
TCA9548A (bus);
Then, we use the usual functions to get readings from the sensor.
Serial.print("Sensor number on bus");
Serial.println(bus);
Serial.print("Temperature = ");
Serial.print(bme.readTemperature());
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bme.readPressure() / 100.0F);
Serial.println(" hPa");
Serial.print("Approx. Altitude = ");
Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(" m");
Serial.print("Humidity = ");
Serial.print(bme.readHumidity());
Serial.println(" %");
setup()
In the setup(), initialize a serial communication for debugging purposes.
Serial.begin(115200);
Start I2C communication on the default I2C pins with the I2C multiplexer.
Wire.begin();
Then, initialize each sensor. The following lines show an example for the first BME280 sensor (it is connected to bus number 2, and it refers to the bme1 instance).
//Init sensor on bus number 2
TCA9548A(2);
if (!bme1.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor on bus 2, check wiring!");
while (1);
}
Serial.println();
Initializing the other sensors is similar, but you need to call the TCA9548A() function with the corresponding I2C bus number. Also, don't forget that each sensor has its own instance.
loop()
In the loop(), we call the printValues() function for each sensor.
printValues(bme1, 2);
printValues(bme2, 3);
printValues(bme3, 4);
printValues(bme4, 5);
And that's pretty much how the code works.
Demonstration
Upload the code to your board. Open the Serial Monitor at a baud rate of 115200. The readings for each sensor will be displayed on the Serial Monitor.
Wrapping Up
This tutorial taught you how to add more I2C ports to your microcontroller with the TCA9548A I2C multiplexer. This is especially useful if you want to connect multiple devices with the same I2C address. Furthermore, the I2C address of the multiplexer itself can be changed from 0x70 to 0x77. This allows us to connect up to 8 multiplexers simultaneously, which allows you to control 64 devices.
The examples shown throughout this tutorial are compatible with the ESP32, ESP8266, and Arduino boards.
We have an extensive tutorial about I2C functions with the ESP32:
ESP32 I2C Communication: Set Pins, Multiple Bus Interfaces and Peripherals (Arduino IDE)
Learn more about ESP32, ESP8266, and Arduino with our resources:
We hope you find this tutorial useful.
with Stepper Motor (28BYJ-48 and ULN2003 Motor Driver)
In this guide, you'll learn how to control a stepper motor with the ESP32. We'll use the 28BYJ-48 unipolar stepper motor with the ULN2003 motor driver. The ESP32 board will be programmed using Arduino IDE.
We have a similar tutorial for the ESP8266 board: ESP8266 NodeMCU with Stepper Motor (28BYJ-48 and ULN2003 Motor Driver)
We have tutorials for other motors with the ESP32:
ESP32 with DC Motor and L298N Motor Driver Control Speed and Direction
ESP32 Servo Motor Web Server with Arduino IDE
Parts Required
To follow this tutorial, you need the following parts:
28BYJ-48 Stepper Motor
ULN2003 Motor Driver
ESP32 (read Best ESP32 Development Boards)
Jumper Wires
5V Power Supply
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Introducing Stepper Motors
A stepper motor is a brushless DC electric motor that divides a full rotation into a number of steps. It moves one step at a time, and each step is the same size. This allows us to rotate the motor by a precise angle to a precise position. The stepper motor can rotate clockwise or counterclockwise.
The following picture shows two 28BYJ-48 stepper motors.
Stepper motors are made of internal coils that make the motor shaft move in steps in one direction or the other when current is applied to the coils in a specific way.
There are two types of stepper motors: unipolar and bipolar stepper motors.
In this article, we won't detail how the stepper motors are made and how they work internally. To learn in more detail how they work and the differences between each type of stepper motor, we recommend reading this article by the DroneBotWorkshop blog.
28BYJ-48 Stepper Motor
There are several stepper motors with different specifications. This tutorial will cover the widely used 28BYJ-48 unipolar stepper motor with the ULN2003 motor driver.
28BYJ-48 Stepper Motor Features
Features of the stepper motor (for more details, consult the datasheet):
Rated voltage: 5V DC
Number of phases: 4
Speed variation ratio: 1/64
Stride angle: 5.625o/64
Frequency: 100Hz
The 28BYJ-48 stepper motor has a total of four coils. One end of the coils is connected to 5V, which corresponds to the motor's red wire. The other end of the coils corresponds to the wires with blue, pink, yellow, and orange color. Energizing the coils in a logical sequence makes the motor move one step in one direction or the other.
The 28BYJ-48 Stepper Motor has a stride angle of 5.625/64 in half-step mode. This means that the motor has a step angle of 5.625oso it needs 360o/5.625o = 64 steps in half-step mode. In full-step mode: 64/2 = 32 steps to complete one rotation.
However, the output shaft is driven via a 64:1 gear ratio. This means that the shaft (visible outside the motor) will complete a rotation if the motor inside rotates 64 times. This means that the motor will have to move 3264 = 2048 steps for the shaft to complete one full rotation. This means that you'll have a precision of 360o/2048 steps = 0.18o/step.
So, in summary:
Total steps per revolution = 2048 steps
Step angle = 0.18o/step
If you're using a different stepper motor, please consult the datasheet.
ULN2003 Motor Driver
To interface the stepper motor with the ESP32, we'll use the ULN2003 motor driver, as shown in the figure below. The 28BYJ-48 stepper motor is many times sold together with the ULN2003 motor driver.
The module comes with a connector that makes it easy and simple to connect the motor to the module. It has four input pins to control the coils that make the stepper motor move. The four LEDs provide a visual interface of the coils' state.
There are pins to connect VCC and GND, and a jumper cap that acts as an ON/OFF switch to power the stepper motorif you remove the jumper, there is no power reaching the motor. You can use those pins to wire a physical switch.
ULN2003 Motor Driver Pinout
The following table shows the module pinout:
IN1 |
Control the motor: connect to a microcontroller digital pin |
IN2 |
Control the motor: connect to a microcontroller digital pin |
IN3 |
Control the motor: connect to a microcontroller digital pin |
IN4 |
Control the motor: connect to a microcontroller digital pin |
VCC |
Powers the motor |
GND |
Common GND |
Motor connector |
Connect the motor connector |
Wire Stepper Motor to the ESP32
In this section, we'll connect the stepper motor to the ESP32 via the ULN2003 motor driver.
We'll connect IN1, IN2, IN3, and IN4 to GPIOs 19, 18, 5, and 17. You can use any other suitable digital pins (check our ESP32 pinout reference guide).
You can follow the next schematic diagram.
Note: you should power the motor driver using an external 5V power supply.
Motor Driver |
ESP32 |
IN1 |
GPIO 19 |
IN2 |
GPIO 18 |
IN3 |
GPIO 5 |
IN4 |
GPIO 17 |
Control Stepper Motor with the ESP32 Code
There are different ways to control stepper motors with a microcontroller. We'll use the Arduino built-in Stepper.h library. This library provides an easy way to move the motor by a defined number of steps.
Copy the following code to your Arduino IDE.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-stepper-motor-28byj-48-uln2003/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Based on Stepper Motor Control - one revolution by Tom Igoe
*/
#include <Stepper.h>
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
// ULN2003 Motor Driver Pins
#define IN1 19
#define IN2 18
#define IN3 5
#define IN4 17
// initialize the stepper library
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
void setup() {
// set the speed at 5 rpm
myStepper.setSpeed(5);
// initialize the serial port
Serial.begin(115200);
}
void loop() {
// step one revolution in one direction:
Serial.println("clockwise");
myStepper.step(stepsPerRevolution);
delay(1000);
// step one revolution in the other direction:
Serial.println("counterclockwise");
myStepper.step(-stepsPerRevolution);
delay(1000);
}
View raw code
We adapted this code from the examples provided by the Stepper library (File > Examples > Stepper > stepper_oneRevolution).
How the Code Works
First, include the Stepper.h library.
#include <Stepper.h>
Define the steps per revolution of your stepper motorin our case, it's 2048:
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
Define the motor input pins. In this example, we're connecting to GPIOs 19, 18, 5, and 17, but you can use any other suitable GPIOs.
#define IN1 19
#define IN2 18
#define IN3 5
#define IN4 17
Initialize an instance of the stepper library called myStepper. Pass as arguments the steps per revolution and the input pins. In the case of the 28BYJ-48 stepper motor, the order of the pins is IN1, IN3, IN2, IN4it might be different for your motor.
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
In the setup(), set the stepper speed using the setSpeed method. The stepper speed is in rpm.
myStepper.setSpeed(5);
Initialize the Serial Monitor at a baud rate of 115200.
Serial.begin(115200);
In the loop(), we'll rotate the stepper motor clockwise and counterclockwise. You can use the step() method on the myStepper object. Pass as an argument the number of steps you want to take. For a full rotation (revolution), you need 2048 steps (stepsPerRevolution variable).
myStepper.step(stepsPerRevolution);
To rotate the motor counterclockwise, you need to pass the number of steps with the minus sign.
myStepper.step(-stepsPerRevolution);
Demonstration
Upload the code to your board. After uploading, the motor will make one clockwise rotation and a counterclockwise rotation over and over again.
You can watch a quick video demonstration:
Other Libraries
Using the Stepper.h library is one of the easiest ways to control a stepper motor. However, if you want more control over your stepper motor, there are libraries with more functions like the AccelStepper library. This library is well documented, with all the methods described in great detail.
It provides several examples that are compatible with the ESP32. Just make sure you initialize a stepper object with the right pins:
AccelStepper stepper (AccelStepper::FULL4WIRE, 19, 5, 18, 17);
This library allows you to control the motors in a non-blocking way and allows you to control more than one motor at a time. But this is a subject for another tutorial.
Wrapping Up
This tutorial was a getting started guide for stepper motors with the ESP32. Stepper motors move one step at a time and allow you to position the motor shaft at a specific angle.
One of the easiest ways to control the stepper motor is to use the built-in Arduino Stepper library. This library provides an easy way to rotate the motor clockwise or counterclockwise a determined number of steps. If you want more control over your stepper motor, we recommend the AccelStepper library.
WebSerial: Web-based Remote Serial Monitor
In this guide, you'll learn how to create and use a web-based Serial Monitor for your ESP32 projects using the WebSerial library. This creates a web-based interface to output debugging messages, as you would do with a regular serial monitor. You can also send messages from the web-based serial monitor to the ESP32.
We have a similar tutorial for the ESP8266 board: ESP8266 NodeMCU WebSerial Web-based Remote Serial Monitor
Web-based Serial Monitor
In most of your ESP32 projects, you use the serial monitor to output debugging messages that help you better understand what's happening with the microcontroller.
You create a Serial communication between your board and your computer, and then you can visualize the messages using the serial monitor. However, when your board is not connected to your computer, you can't see the debugging messages.
A workaround for this issue is to use a web-based serial monitorthe ESP32 hosts a web server that serves a page to visualize the messages as you would with the regular serial monitor. The WebSerial web page also allows you to send data from the web page to your board.
For this tutorial, we'll use the WebSerial library.
If you like this library and you'll use it in your projects, consider supporting the developer's work.
WebSerial Features
List of WebSerial features:
Works on WebSockets;
Realtime logging;
Any number of serial monitors can be opened on the browser;
Uses AsyncWebserver for better performance.
WebSerial Functions
Using WebSerial is similar to use the serial monitor. Its main functions are print() and println():
print(): prints the data on the web-based serial monitor without newline character (on the same line);
println(): prints the data on the web-based serial monitor with a newline character (on the next line);
Installing the WebSerial Library
For this project, we'll use the WebSerial.h library. To install the library:
In your Arduino IDE, go to Sketch > Include Library > Manage Libraries
Search for webserial.
Install the WebSerial library by Ayush Sharma.
You also need to install the ESPAsyncWebServer and the AsyncTCP libraries. Click the following links to download the libraries' files.
ESPAsyncWebServer
AsyncTCP
To install these libraries, click on the previous links to download the libraries' files. Then, in your Arduino IDE, go to Sketch > Include Library > Add .ZIP Library
If you're using VS Code with the PlatformIO extension, copy the following to the platformio.ini file to include the libraries.
lib_deps = ESP Async WebServer
ayushsharma82/WebSerial @ ^1.1.0
ESP32 WebSerial Example
The library provides a simple example about creating the Web Serial Monitor to output and receive messages. We've modified the example a bit to make it more interactive.
This example prints Hello! to the web-based serial monitor every two seconds. Additionally, you can send messages from the web-based serial monitor to the board. You can send the message ON to light up the board's built-in LED or the message OFF to turn it off.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-webserial-library/
This sketch is based on the WebSerial library example: ESP32_Demo
https://github.com/ayushsharma82/WebSerial
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <WebSerial.h>
#define LED 2
AsyncWebServer server(80);
const char* ssid = "REPLACE_WITH_YOUR_SSID"; // Your WiFi SSID
const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Your WiFi Password
void recvMsg(uint8_t *data, size_t len){
WebSerial.println("Received Data...");
String d = "";
for(int i=0; i < len; i++){
d += char(data[i]);
}
WebSerial.println(d);
if (d == "ON"){
digitalWrite(LED, HIGH);
}
if (d=="OFF"){
digitalWrite(LED, LOW);
}
}
void setup() {
Serial.begin(115200);
pinMode(LED, OUTPUT);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.printf("WiFi Failed!\n");
return;
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
// WebSerial is accessible at "<IP Address>/webserial" in browser
WebSerial.begin(&server);
WebSerial.msgCallback(recvMsg);
server.begin();
}
void loop() {
WebSerial.println("Hello!");
delay(2000);
}
View raw code
Before uploading the code to your board, don't forget to insert your network credentials.
In this example, the ESP32 is in station mode. This example also works in access point mode. To learn how to set up your ESP32 as an access point, read:
How to Set an ESP32 Access Point (AP) for Web Server
How the Code Works
Continue reading to learn how the code works or skip to the demonstration section.
First, you need to include the required libraries for WebSerial. The WiFi.h library is needed to connect the ESP32 to a Wi-Fi network.
#include <WiFi.h>
The WebSerial library uses the AsyncTCP and the ESPAsyncWebServer libraries to create the web-based serial monitor.
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
Finally, the WebSerial library provides easy methods to build the web-based serial monitor.
#include <WebSerial.h>
Create a variable called LED for the built-in LED on GPIO 2.
#define LED 2
Initialize an AsyncWebServer object on port 80 to set up the web server.
AsyncWebServer server(80);
Insert your network credentials in the following variables:
const char* ssid = "REPLACE_WITH_YOUR_SSID"; // Your WiFi SSID
const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Your WiFi Password
Handling Received Messages
The following function receives incoming messages sent from the web-based serial monitor. The message is saved on the d variable. Then, it is printed on the web serial monitor using WebSerial.println(d).
void recvMsg(uint8_t *data, size_t len){
WebSerial.println("Received Data...");
String d = "";
for(int i=0; i < len; i++){
d += char(data[i]);
}
WebSerial.println(d);
Next, we check if the content of the d variable is ON or OFF and light up the LED accordingly.
if (d == "ON"){
digitalWrite(LED, HIGH);
}
if (d=="OFF"){
digitalWrite(LED, LOW);
}
setup()
In the setup(), set the LED as an OUTPUT.
pinMode(LED, OUTPUT);
Connect your board to your local network:
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.printf("WiFi Failed!\n");
return;
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
Initialize the web-based serial monitor with the begin() method on the WebSerial object. This function accepts as an argument an AsyncWebServer object.
WebSerial.begin(&server);
Register the recvMsg() as a callback function using the msgCallback() method on the WebSerial object. The recvMsg() function will run whenever you send a message from the monitor to the board.
WebSerial.msgCallback(recvMsg);
Finally, initialize the server.
server.begin();
It is just after calling this line that the web-based serial monitor will start working.
loop()
In the loop(), print the Hello! message every 2000 milliseconds (2 seconds) using the println() function on the WebSerial object.
void loop() {
WebSerial.println("Hello!");
delay(2000);
}
Demonstration
After inserting your network credentials, you can upload the code to your board.
After uploading, open the regular serial monitor at a baud rate of 115200. The board's IP address will be printed.
Now, open a browser on your local network and type the ESP IP address followed by /webserial. For example, in my case:
192.168.1.85/webserial
The WebSerial page should load.
As you can see, it is printing Hello! every two seconds. Additionally, you can send commands to the ESP32. All the commands that you send are printed back on the web serial monitor. You can send the ON and OFF commands to control the built-in LED.
This was just a simple example showing how you can use the WebSerial library to create a web-based serial monitor to send and receive data.
Now, you can easily add a web-based serial monitor to any of your projects using the WebSerial library.
Wrapping Up
In this quick tutorial, you learned how to create a web-based serial monitor. This is especially useful if your project is not connected to your computer via Serial communication and you still want to visualize debugging messages. The communication between the web-based serial monitor and the ESP32 uses WebSocket protocol.
We hope you find this tutorial useful. We have other web server tutorials you may like:
ESP32 Web Server using Server-Sent Events (Update Sensor Readings Automatically)
ESP32 WebSocket Server: Control Outputs (Arduino IDE)
ESP32 OTA (Over-the-Air) Updates AsyncElegantOTA using Arduino IDE
Altimeter Datalogger: ESP32 with BMP388, MicroSD Card Storage and OLED Display
In this project, we'll build an altimeter datalogger with the ESP32 and the BMP388 sensor. The BMP388 is a precise pressure sensor that allows us to estimate altitude with great accuracy. In this project, the pressure and altitude are logged to a file on a microSD. We've also added an OLED display to this project so that you can check the current altitude by pressing a pushbutton.
For a getting started guide for the BMP388, check the following tutorial: ESP32 with BMP388 Barometric/Altimeter Sensor (Arduino IDE)
Table of Contents:
Project Overview
Parts Required
Prerequisites
Schematic Diagram (circuit)
Code and How it Works
Demonstration
Wrapping Up
Why we created this project
Project Overview
Before going straight to the project, let's look at the main features of this project.
The ESP32 sets an access point (1) that you can connect to using your smarpthone. Once connected, you can access a web page with an input field, where you can enter the current sea level pressure at your location. You must enter this value when the project starts running. The altitude is calculated comparaing the sensor's pressure with the current sea level pressure, that's why this step is important for accurate results. You can check the current sea level pressure.
The ESP32 is connected to a BMP388 pressure sensor, a microSD card module, an OLED display and a pushbutton.
Every minute (or other period of time you define in the code), the ESP32 records new sensor readings to a file on the microSD card (2). It records current seal level pressure, current pressure at the sensor's location, temperature and altitude estimation.
When you press the pushbutton, the OLED display turns on and shows the current altitude and temperature (3). After 5 seconds, it turns off (to save power).
To better understand how this project works, it might be helpful to take a look at the following tutorials:
ESP32 with BMP388 Barometric/Altimeter Sensor (Arduino IDE)
ESP32: Guide for MicroSD Card Module using Arduino IDE
ESP32 OLED Display with Arduino IDE
Input Data on HTML Form ESP32/ESP8266 Web Server using Arduino IDE
How to Set an ESP32 Access Point (AP) for Web Server
Parts Required
To build this project, you need the following parts:
DOIT ESP32 DEVKIT V1 Board read Best ESP32 Development Boards
BMP388 sensor module (Guide for BMP388)
MicroSD card module + microSD card
Pushbutton
10k Ohm resistor
Breadboard
Jumper wires
Prerequisites
Before continuing, make sure you check the following prerequisites.
1. Install ESP32 Board in Arduino IDE
We'll program the ESP32 using Arduino IDE. So, you must have the ESP32 add-on installed. Follow the next tutorial if you haven't already:
Installing ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP32:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
2. Installing Libraries
To build this project, you need to install the following libraries:
Adafruit BMP3XX library (Arduino Library Manager)
Adafruit_Sensor library (Arduino Library Manager)
Adafruit_SSD1306 library (Arduino Library Manager)
Adafruit_GFX library (Arduino Library Manager)
ESPAsyncWebServer (.zip folder);
AsyncTCP (.zip folder).
You can install the first four libraries using the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the library name.
The ESPAsyncWebServer and AsynTCP libraries aren't available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch > Include Library > Add .zip Library and select the libraries you've just downloaded.
Installing Libraries (VS Code + PlatformIO)
If you're programming the ESP32 using PlatformIO, you should add the following lines to the platformio.ini file to include the libraries (also change the Serial Monitor speed to 115200):
monitor_speed = 115200
lib_deps = ESP Async WebServer
adafruit/Adafruit SSD1306@^2.4.6
adafruit/Adafruit GFX Library@^1.10.10
adafruit/Adafruit BMP3XX Library@^2.1.0
3. Formatting the MicroSD Card
Before proceeding with the tutorial, make sure you format your microSD card as FAT32. Follow the next instructions to format your microSD card or use a software tool like SD Card Formater (compatible with Windows and Mac OS).
1. Insert the microSD card into your computer. Go to My Computer and right-click on the SD card. Select Format as shown in the figure below.
2. A new window pops up. Select FAT32, press Start to initialize the formatting process, and follow the onscreen instructions.
Schematic Diagram
Connect all the components as shown in the following schematic diagram.
You can also check the wiring in the following table:
Component |
ESP32 Pin Assignment |
BMP388 |
GPIO 21 (SDA), GPIO 22 (SCL) |
OLED Display |
GPIO 21 (SDA), GPIO 22 (SCL) |
MicroSD card Module |
GPIO 5 (CS), GPIO 23 (MOSI), GPIO 18 (CLK), GPIO 19 (MISO) |
Pushbutton |
GPIO 4 |
Code
Copy the following code to your ESP32 board, and the project will work straight away.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/altimeter-datalogger-esp32-bmp388/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BMP3XX.h"
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
//Libraries for microSD card
#include "FS.h"
#include "SD.h"
#include "SPI.h"
AsyncWebServer server(80);
// Replace with your network credentials
const char* ssid = "ESP32";
const char* password = NULL;
//OLED Display
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Variables for BMP388
float seaLevelPressure = 1013.25;
Adafruit_BMP3XX bmp;
float alt;
float temp;
float pres;
String dataMessage;
//Pushbutton
const int buttonPin = 4;
int buttonState;
int lastButtonState = LOW;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50; // the debounce time; increase if the output flickers
//Timers for datalogging
unsigned long lastTimer = 0;
unsigned long timerDelay = 18000;
const char* PARAM_INPUT_1 = "seaLevelPressure";
// HTML web page to handle 1 input field
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head>
<title>Sea Level Pressure</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head><body>
<form action="/get">
Sea Level Pressure: <input type="float" name="seaLevelPressure">
<input type="submit" value="Submit">
</form>
</body></html>)rawliteral";
void initBMP(){
if (!bmp.begin_I2C()) { // hardware I2C mode, can pass in address & alt Wire
//if (! bmp.begin_SPI(BMP_CS)) { // hardware SPI mode
//if (! bmp.begin_SPI(BMP_CS, BMP_SCK, BMP_MISO, BMP_MOSI)) { // software SPI mode
Serial.println("Could not find a valid BMP3 sensor, check wiring!");
while (1);
}
// Set up oversampling and filter initialization
bmp.setTemperatureOversampling(BMP3_OVERSAMPLING_8X);
bmp.setPressureOversampling(BMP3_OVERSAMPLING_4X);
bmp.setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3);
bmp.setOutputDataRate(BMP3_ODR_50_HZ);
}
void getReadings(){
if (! bmp.performReading()) {
Serial.println("Failed to perform reading :(");
return;
}
temp = bmp.temperature;
pres = bmp.pressure / 100.0;
alt = bmp.readAltitude(seaLevelPressure);
}
void initDisplay(){
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
delay(500);
display.clearDisplay();
display.setTextColor(WHITE);
}
void displayReadings(){
display.clearDisplay();
// display temperature
display.setTextSize(1);
display.setCursor(0,0);
display.print("Temperature: ");
display.setTextSize(2);
display.setCursor(0,10);
display.print(String(temp));
display.print(" ");
display.setTextSize(1);
display.cp437(true);
display.write(167);
display.setTextSize(2);
display.print("C");
// display altitude
display.setTextSize(1);
display.setCursor(0, 35);
display.print("Altitude: ");
display.setTextSize(2);
display.setCursor(0, 45);
display.print(String(alt));
display.print(" m");
display.display();
}
// Initialize SD card
void initSDCard(){
if (!SD.begin()) {
Serial.println("Card Mount Failed");
return;
}
}
// Write to the SD card
void writeFile(fs::FS &fs, const char * path, const char * message) {
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file) {
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)) {
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
// Append data to the SD card
void appendFile(fs::FS &fs, const char * path, const char * message) {
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file) {
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)) {
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
// Initialize WiFi
void initWiFi() {
WiFi.softAP(ssid, password);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
}
void setup() {
Serial.begin(115200);
initBMP();
initDisplay();
initSDCard();
initWiFi();
pinMode(buttonPin, INPUT);
File file = SD.open("/data.txt");
if(!file) {
Serial.println("File doesn't exist");
Serial.println("Creating file...");
writeFile(SD, "/data.txt", "Pressure, Altitude, Temperature \r\n");
}
else {
Serial.println("File already exists");
}
file.close();
// Send web page with input fields to client
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html);
});
// Send a GET request to <ESP_IP>/get?input1=<inputMessage>
server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
String inputMessage;
// GET input1 value on <ESP_IP>/get?input1=<inputMessage>
if (request->hasParam(PARAM_INPUT_1)) {
inputMessage = request->getParam(PARAM_INPUT_1)->value();
seaLevelPressure = inputMessage.toFloat();
}
else {
inputMessage = "No message sent";
}
Serial.println(inputMessage);
request->send(200, "text/html", "HTTP GET request sent to your ESP on input field with value: " + inputMessage +
"<br><a href=\"/\">Return to Home Page</a>");
});
server.begin();
}
void loop() {
int reading = digitalRead(buttonPin);
display.clearDisplay();
// Light up when the pushbutton is pressed
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != buttonState) {
buttonState = reading;
if (buttonState == HIGH) {
getReadings();
displayReadings();
delay(5000);
display.clearDisplay();
display.display();
lastDebounceTime = millis();
}
}
}
lastButtonState = reading;
// Log data every timerDelay seconds
if ((millis() - lastTimer) > timerDelay) {
//Concatenate all info separated by commas
getReadings();
dataMessage = String(pres) + "," + String(alt) + "," + String(temp)+ "," + String(seaLevelPressure) + "\r\n";
Serial.print(dataMessage);
//Append the data to file
appendFile(SD, "/data.txt", dataMessage.c_str());
lastTimer = millis();
}
}
View raw code
How the Code Works
Continue reading to learn how the code works, or skip to the demonstration section.
Start by including all the necessary libraries:
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BMP3XX.h"
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
//Libraries for microSD card
#include "FS.h"
#include "SD.h"
#include "SPI.h"
The following lines set the name and password for the access point. In this case, we set the password to NULLthis creates an open access point. You can add a password for the access point if you want.
// Replace with your network credentials
const char* ssid = "ESP32";
const char* password = NULL;
Set the OLED display size and instantiate an instance on the default I2C pins.
// OLED Display
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
We set the default sea level pressure as 1013.25 hPa. However, you should connect to the access point to change this value for more accurate results.
float seaLevelPressure = 1013.25;
Create an instance for the BMP388 sensor called bmpthis automatically uses the default I2C pins.
Adafruit_BMP3XX bmp;
The following variables will be used to save the sensor data.
float alt;
float temp;
float pres;
String dataMessage;
The pushbutton is connected to GPIO 4.
const int buttonPin = 4;
The buttonState and lastButtonState variables save the current button state and the last button state.
int buttonState;
int lastButtonState = LOW;
The next variables are used to debounce the pushbutton.
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50; // the debounce time; increase if the output flickers
The BMP388 readings are saved every minute. You can change that in the timerDelay variable (in milliseconds).
//Timers for datalogging
unsigned long lastTimer = 0;
unsigned long timerDelay = 60000;
The PARAM_INPUT_1 variable will be used to search for the input value on the HTML form. To learn more about HTML forms with the ESP32, we recommend this tutorial.
const char* PARAM_INPUT_1 = "seaLevelPressure";
The index_html variable saves a simple HTML page that displays an input field to enter the current sea level pressure.
// HTML web page to handle 1 input field
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head>
<title>Sea Level Pressure</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head><body>
<form action="/get">
Sea Level Pressure: <input type="float" name="seaLevelPressure">
<input type="submit" value="Submit">
</form>
</body></html>)rawliteral";
When you submit a new pressure value, the ESP32 receives a request on the following URL (for example, pressure = 1022):
/get?seaLevelPressure=1022
Initialize BMP388
The initBMP() function initializes the BMP388 sensor. Read the ESP32 with the BMP388 tutorial to learn more.
void initBMP(){
if (!bmp.begin_I2C()) { // hardware I2C mode, can pass in address & alt Wire
//if (! bmp.begin_SPI(BMP_CS)) { // hardware SPI mode
//if (! bmp.begin_SPI(BMP_CS, BMP_SCK, BMP_MISO, BMP_MOSI)) { // software SPI mode
Serial.println("Could not find a valid BMP3 sensor, check wiring!");
while (1);
}
// Set up oversampling and filter initialization
bmp.setTemperatureOversampling(BMP3_OVERSAMPLING_8X);
bmp.setPressureOversampling(BMP3_OVERSAMPLING_4X);
bmp.setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3);
bmp.setOutputDataRate(BMP3_ODR_50_HZ);
}
Get BMP388 Readings
The getReadings() function gets new readings: temperature, pressure, and altitude and saves them on the temp, pres, and alt variables.
void getReadings(){
if (! bmp.performReading()) {
Serial.println("Failed to perform reading :(");
return;
}
temp = bmp.temperature;
pres = bmp.pressure / 100.0;
alt = bmp.readAltitude(seaLevelPressure);
}
Initialize OLED Display
The initDisplay() function initializes the OLED display.
void initDisplay(){
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
delay(500);
display.clearDisplay();
display.setTextColor(WHITE);
}
To learn more about the OLED display with the ESP32, read ESP32 OLED Display with Arduino IDE.
Display BMP388 Readings
The displayReadings() function displays the temperature and altitude on the display.
void displayReadings(){
display.clearDisplay();
// display temperature
display.setTextSize(1);
display.setCursor(0,0);
display.print("Temperature: ");
display.setTextSize(2);
display.setCursor(0,10);
display.print(String(temp));
display.print(" ");
display.setTextSize(1);
display.cp437(true);
display.write(167);
display.setTextSize(2);
display.print("C");
// display altitude
display.setTextSize(1);
display.setCursor(0, 35);
display.print("Altitude: ");
display.setTextSize(2);
display.setCursor(0, 45);
display.print(String(alt));
display.print(" m");
display.display();
}
Initialize microSD card
The initSDCard() function initializes the microSD card on the default SPI pins.
// Initialize SD card
void initSDCard(){
if (!SD.begin()) {
Serial.println("Card Mount Failed");
return;
}
}
If you want to use other pins, read this article to learn how to set custom SPI pins.
Write to the microSD card
The writeFile() and appendFile() functions write and append a message to a file on the microSD card.
// Write to the SD card
void writeFile(fs::FS &fs, const char * path, const char * message) {
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file) {
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)) {
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
// Append data to the SD card
void appendFile(fs::FS &fs, const char * path, const char * message) {
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file) {
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)) {
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
To learn more about microSD card functions, read ESP32: Guide for MicroSD Card Module using Arduino IDE.
Set Access Point
Initialize Wi-Fi by setting the ESP32 as an access point.
// Initialize WiFi
void initWiFi() {
WiFi.softAP(ssid, password);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
}
setup()
In the setup(), initialize the Serial Monitor, the BMP388 sensor, the OLED display, the microSD card, start the access point and define the pushbutton as an INPUT.
Serial.begin(115200);
initBMP();
initDisplay();
initSDCard();
initWiFi();
pinMode(buttonPin, INPUT);
Create a new file on the microSD card called data.txt if it doesn't exist already.
File file = SD.open("/data.txt");
if(!file) {
Serial.println("File doesn't exist");
Serial.println("Creating file...");
writeFile(SD, "/data.txt", "Pressure, Altitude, Temperature \r\n");
}
else {
Serial.println("File already exists");
}
file.close();
When you access the Access Point on the root (/) URL, the server (ESP32) sends the HTML page (index_html variable) with the form.
// Send web page with input fields to client
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html);
});
The following part gets the input field you've inserted in the form and saves it in the seaLevelPressure variable.
// Send web page with input fields to client
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html);
});
// Send a GET request to <ESP_IP>/get?input1=<inputMessage>
server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
String inputMessage;
// GET input1 value on <ESP_IP>/get?input1=<inputMessage>
if (request->hasParam(PARAM_INPUT_1)) {
inputMessage = request->getParam(PARAM_INPUT_1)->value();
seaLevelPressure = inputMessage.toFloat();
}
else {
inputMessage = "No message sent";
}
Serial.println(inputMessage);
request->send(200, "text/html", "HTTP GET request sent to your ESP on input field with value: " + inputMessage +
"<br><a href=\"/\">Return to Home Page</a>");
});
To learn more about how this works, read: Input Data on HTML Form ESP32/ESP8266 Web Server using Arduino IDE.
loop()
In the loop() is where we check the state of the pushbutton. If it was pressed, we light up the OLED display with the current temperature and altitude readings.
int reading = digitalRead(buttonPin);
display.clearDisplay();
// Light up when the pushbutton is pressed
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != buttonState) {
buttonState = reading;
if (buttonState == HIGH) {
getReadings();
displayReadings();
delay(5000);
display.clearDisplay();
display.display();
lastDebounceTime = millis();
}
}
}
lastButtonState = reading;
We also save new readings every 60 seconds (timerDelay) variable.
if ((millis() - lastTimer) > timerDelay) {
//Concatenate all info separated by commas
getReadings();
dataMessage = String(pres) + "," + String(alt) + "," + String(temp)+ "," + String(seaLevelPressure) + "\r\n";
Serial.print(dataMessage);
//Append the data to file
appendFile(SD, "/data.txt", dataMessage.c_str());
lastTimer = millis();
}
Demonstration
Upload the code to your board. Don't forget to select the right board (ESP32) and COM port.
After uploading, you should get the following messages on the Serial Monitor*.
*In my case, it shows File already exists. But, when you're running it for the first time, it should show File doesn't exist, Creating file .
After a few seconds, it will display the first readings.
After that, if you want to get more accurate altitude readings, on your computer or smartphone, connect to the ESP32 access point. Open a browser and go to the 192.168.4.1 IP address and insert the current sea level pressure at your location.
After you've clicked the submit button, you'll see the inserted value on the Serial Monitor.
If you click on the pushbutton, you can check the current temperature and altitude on the OLED display.
If you want to check all the readings, you just need to connect the microSD card to your computer, and you can access the data.txt file with all the records. To analyze your data, you can use Google Sheets, Excel, or other software.
Wrapping Up
This project showed you how to log data to a microSD card using the ESP32 and how to get altitude using the BMP388 pressure sensor. You also learned how to create an access point where you can enter data that the ESP32 will uselike the sea level pressure.
We have a similar project using a temperature sensor (it connects to an NTP server to timestamp the readings, so the ESP32 needs to have access to the internet):
ESP32 Data Logging Temperature to MicroSD Card
with HC-SR04 Ultrasonic Sensor with Arduino IDE
This guide shows how to use the HC-SR04 Ultrasonic Sensor with the ESP32 board using the Arduino core. The ultrasonic sensor uses sonar to determine the distance to an object. We'll show you how to wire the sensor to the ESP32 and provide several example sketches to determine the distance to an object using the HC-SR04.
This tutorial covers the following topics:
Ultrasonic Sensor HC-SR04 Pinout
Wiring the Ultrasonic Sensor HC-SR04 to the ESP32
Getting Distance to an Object Using the Ultrasonic Sensor HC-SR04 with the ESP32
Displaying the Distance to an Object on a Display Using the ESP32 and HC-SR04
Introducing the HC-SR04 Ultrasonic Sensor
The HC-SR04 ultrasonic sensor uses sonar to determine the distance to an object. This sensor reads from 2cm to 400cm (0.8inch to 157inch) with an accuracy of 0.3cm (0.1inches), which is good for most hobbyist projects. In addition, this particular module comes with ultrasonic transmitter and receiver modules.
The following picture shows the HC-SR04 ultrasonic sensor.
The next picture shows the other side of the sensor.
Where to buy HC-SR04 Ultrasonic Sensor?
You can check the Ultrasonic Sensor HC-SR04 sensor on Maker Advisor to find the best price:
HC-SR04 Ultrasonic Sensor
HC-SR04 Ultrasonic Sensor Technical Data
The following table shows the key features and specs of the HC-SR04 ultrasonic sensor. For more information, you should consult the sensor's datasheet.
Power Supply |
5V DC |
Working Current |
15 mA |
Working Frequency |
40 kHz |
Maximum Range |
4 meters |
Minimum Range |
2 cm |
Measuring Angle |
15o |
Resolution |
0.3 cm |
Trigger Input Signal |
10uS TTL pulse |
Echo Output Signal |
TTL pulse proportional to the distance range |
Dimensions |
45mm x 20mm x 15mm |
HC-SR04 Ultrasonic Sensor Pinout
Here's the pinout of the HC-SR04 Ultrasonic Sensor.
VCC |
Powers the sensor (5V) |
Trig |
Trigger Input Pin |
Echo |
Echo Output Pin |
GND |
Common GND |
How Does the HC-SR04 Ultrasonic Sensor Work?
The ultrasonic sensor uses sonar to determine the distance to an object. Here's how it works:
The ultrasound transmitter (trig pin) emits a high-frequency sound (40 kHz).
The sound travels through the air. If it finds an object, it bounces back to the module.
The ultrasound receiver (echo pin) receives the reflected sound (echo).
Taking into account the sound's velocity in the air and the travel time (time passed since the transmission and reception of the signal) we can calculate the distance to an object. Here's the formula:
distance to an object = ((speed of sound in the air)*time)/2
speed of sound in the air at 20oC (68oF) = 343m/s
Parts Required
To complete this tutorial you need the following parts:
HC-SR04 Ultrasonic Sensor
ESP32 (read Best ESP32 development boards)
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic ESP32 with HC-SR04 Ultrasonic Sensor
Wire the HC-SR04 ultrasonic sensor to the ESP32 as shown in the following schematic diagram. We're connecting the Trig pin to GPIO 5 and the Echo pin to GPIO 18, but you can use any other suitable pins.
Ultrasonic Sensor |
ESP32 |
VCC |
VIN |
Trig |
GPIO 5 |
Echo |
GPIO 18 |
GND |
GND |
Preparing Arduino IDE
We'll program the ESP32 board using Arduino IDE. So, make sure you have the ESP32 add-on installed. Follow the next tutorial:
Install the ESP32 Board in Arduino IDE
If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP32:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
Code Getting Distance to an Object using the HC-SR04 Ultrasonic Sensor and ESP32
The following sketch is a simple example of how you can get the distance between the sensor and an object using the ESP32 board with the Arduino core.
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-hc-sr04-ultrasonic-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/
const int trigPin = 5;
const int echoPin = 18;
//define sound speed in cm/uS
#define SOUND_SPEED 0.034
#define CM_TO_INCH 0.393701
long duration;
float distanceCm;
float distanceInch;
void setup() {
Serial.begin(115200); // Starts the serial communication
pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output
pinMode(echoPin, INPUT); // Sets the echoPin as an Input
}
void loop() {
// Clears the trigPin
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
// Sets the trigPin on HIGH state for 10 micro seconds
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// Reads the echoPin, returns the sound wave travel time in microseconds
duration = pulseIn(echoPin, HIGH);
// Calculate the distance
distanceCm = duration * SOUND_SPEED/2;
// Convert to inches
distanceInch = distanceCm * CM_TO_INCH;
// Prints the distance in the Serial Monitor
Serial.print("Distance (cm): ");
Serial.println(distanceCm);
Serial.print("Distance (inch): ");
Serial.println(distanceInch);
delay(1000);
}
View raw code
Upload the code to your board and it will work straight away. Continue reading if you want to learn how the code works or skip to the demonstration section.
How the Code Works
First, define the trigger and the echo pins.
const int trigPin = 5;
const int echoPin = 18;
In this example, we're using GPIO 5 and GPIO 18. But you can use any other suitable GPIOsread ESP32 Pinout Reference: Which GPIO pins should you use?
The SOUND_SPEED variable saves the velocity of sound in the air at 20oC. We're using the value in cm/uS.
#define SOUND_SPEED 0.034
The CM_TO_INCH variable allows us to convert distance in centimeters to inches.
#define CM_TO_INCH 0.393701
Then, initialize the following variables.
long duration;
float distanceCm;
float distanceInch;
The duration variable saves the travel time of the ultrasonic waves (time elapsed since transmission and reception of the pulse wave). The distanceCm and distanceInch, as the names suggest, save the distance to an object in centimeters and inches.
setup()
In the setup(), initialize a serial communication at a baud rate of 115200 so that we can print the measurements on the Serial Monitor.
Serial.begin(115200); // Starts the serial communication
Define the trigger pin as an OUTPUTthe trigger pin emits the ultrasound. And define the echo pin as an INPUTthe echo pin receives the reflected wave and sends a signal to the ESP32 that is proportional to the travel time.
pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output
pinMode(echoPin, INPUT); // Sets the echoPin as an Input
loop()
In the loop(), the following lines produce a 10uS HIGH pulse on the trigger pinthis means the pin will emit an ultrasound. Note that before sending the pulse, we give a short LOW pulse to ensure you'll get a clean HIGH pulse.
// Clears the trigPin
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
// Sets the trigPin on HIGH state for 10 micro seconds
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
We use the pulseIn() function to get the sound wave travel time:
duration = pulseIn(echoPin, HIGH);
The pulseIn() function reads a HIGH or a LOW pulse on a pin. It accepts as arguments the pin and the state of the pulse (either HIGH or LOW). It returns the length of the pulse in microseconds. The pulse length corresponds to the time it took to travel to the object plus the time traveled on the way back.
Then, we simply calculate the distance to an object taking into account the sound speed.
distanceCm = duration * SOUND_SPEED/2;
Convert the distance to inches:
distanceInch = distanceCm * CM_TO_INCH;
And finally, print the results on the Serial Monitor.
Serial.print("Distance (cm): ");
Serial.println(distanceCm);
Serial.print("Distance (inch): ");
Serial.println(distanceInch);
Demonstration
Upload the code to your board. Don't forget to select the board you're using in Tools > Boards. Also, don't forget to select the right COM port in Tools > Port.
After uploading, open the Serial Monitor at a baud rate of 115200. Press the on-board RST button to restart the board and it will start printing the distance to the closest object on the Serial Monitor. Something as shown in the picture below.
ESP32 with HC-SR04 and OLED Display
In this section, we'll show you a simple example with the ESP32 that displays the distance on an I2C OLED display.
To better understand how the project works, we recommend taking a look at our ESP32 tutorial with the I2C OLED display.
Parts Required
Here's a list with the parts required to complete this example:
HC-SR04 Ultrasonic Sensor
ESP32 (read Best ESP32 development boards)
0.96 inch I2C OLED Display SSD1306
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic Diagram ESP32 with HC-SR04 and OLED Display
Wire all the parts as shown in the following schematic diagram.
Learn more about the OLED display with the ESP32: ESP32 OLED Display with Arduino IDE
Code ESP32 Display Distance (HC-SR04) on OLED Display
To use this example, make sure you have the Adafruit SSD1306 and Adafruit GFX libraries installed. You can install these libraries through the Arduino Library Manager.
Go to Sketch > Library > Manage Libraries, search for SSD1306, and install the SSD1306 library from Adafruit.
After installing the SSD1306 library from Adafruit, type GFX in the search box and install the library.
After installing the libraries, restart your Arduino IDE.
Then, simply copy the following code to your Arduino IDE and upload the code to the board.
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-hc-sr04-ultrasonic-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
const int trigPin = 5;
const int echoPin = 18;
//define sound speed in cm/uS
#define SOUND_SPEED 0.034
#define CM_TO_INCH 0.393701
long duration;
int distanceCm;
int distanceInch;
void setup() {
Serial.begin(115200);
pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output
pinMode(echoPin, INPUT); // Sets the echoPin as an Input
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
delay(500);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE);
}
void loop() {
// Clears the trigPin
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
// Sets the trigPin on HIGH state for 10 micro seconds
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// Reads the echoPin, returns the sound wave travel time in microseconds
duration = pulseIn(echoPin, HIGH);
// Calculate the distance
distanceCm = duration * SOUND_SPEED/2;
// Convert to inches
distanceInch = distanceCm * CM_TO_INCH;
// Prints the distance in the Serial Monitor
Serial.print("Distance (cm): ");
Serial.println(distanceCm);
Serial.print("Distance (inch): ");
Serial.println(distanceInch);
display.clearDisplay();
display.setCursor(0, 25);
//Display distance in cm
display.print(distanceCm);
display.print(" cm");
// Display distance in inches
/* display.print(distanceInch);
display.print(" in");*/
display.display();
delay(500);
}
View raw code
How the Code Works
Start by including the required libraries for the OLED display:
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Define the width and height of the OLED display. We're using a 12864 OLED display:
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
Create an Adafruit_SSD1306 object called display to handle the OLED display.
Adafruit_SSD1306 display = Adafruit_SSD1306(128, 64, &Wire);
Define the pins that the HC-SR04 sensor is connected to.
const int trigPin = 5;
const int echoPin = 18;
Create variables to save the distance and the duration between the transmission and reception of the sound waves.
long duration;
int distanceCm;
int distanceInch;
setup()
In the setup(), initialize a serial communication at a baud rate of 115200 so that we can print the results on the Serial Monitor.
Serial.begin(115200);
Define the trigger pin as an OUTPUT and the echo pin as an INPUT.
pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output
pinMode(echoPin, INPUT); // Sets the echoPin as an Input
Initialize the OLED display:
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
Set the font size and color for the display.
display.setTextSize(2);
display.setTextColor(WHITE);
loop()
In the loop() is where we'll get the distance and display it on the OLED.
Get the distance (we've already explained in the previous section how to calculate the distance).
// Clears the trigPin
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
// Sets the trigPin on HIGH state for 10 micro seconds
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// Reads the echoPin, returns the sound wave travel time in microseconds
duration = pulseIn(echoPin, HIGH);
// Calculate the distance
distanceCm = duration * SOUND_SPEED/2;
// Convert to inches
distanceInch = distanceCm * CM_TO_INCH;
Print the distance on the Serial Monitor.
// Prints the distance on the Serial Monitor
Serial.print("Distance (cm): ");
Serial.println(distanceCm);
Serial.print("Distance (inch): ");
Serial.println(distanceInch);
Clear the display in each loop() to write new readings.
display.clearDisplay();
Set the display cursor to (0, 25).
display.setCursor(0, 25);
The following lines print the distance in centimeters in the OLED display.
// Display static text
display.print(distanceCm);
display.print(" cm");
Comment the previous lines and uncomment the following lines if you want to display the readings in inches.
/* Display distance in inches
display.print(distanceInch);
display.print(" in");*/
Lastly, call display.display() to actually show the readings on the OLED.
display.display();
The distance is updated every 500 milliseconds.
delay(500);
Demonstration
Upload the code to your ESP32 board. Go to Tools > Board and select the ESP32 board you're using. Go to Tools > Port and select the port your board is connected to. Then, click the upload button.
Open the Serial Monitor at a baud rate of 115200, press the on-board RST button. The sensor measurements will be displayed both on the Serial Monitor and the OLED display.
Approximate an object to the sensor and see the values changing.
You can watch a quick video demonstration:
Wrapping Up
The HC-SR04 Ultrasonic Sensor allows us to determine the distance to an object. In this tutorial you've learned how to use the HC-SR04 with the ESP32.
VS Code Workspaces with ESP32 and ESP8266 Projects
In this tutorial, you'll learn how to deal with workspaces on VS Code to organize your ESP32 and ESP8266 projects. You'll learn what is a single-folder workspace, a multi-root workspace, the advantages of workspaces, and how to use them.
To get familiar with VS Code with the ESP32 and ESP8266, follow the next tutorial first:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266
You can also use VS Code to program your boards with MicroPython firmware:
MicroPython: Program ESP32/ESP8266 using VS Code and Pymakr
Table of Contents
Throughout this tutorial, we'll cover the following topics:
What is a Workspace (VS Code)?
Single-folder Workspace
Multi-root Workspace
Advantages of Workspaces
Add Folders to Workspace
Save Workspace
Workspace File
Workspace Settings
Workspace Enabled/Disabled Extensions
Upload a Project in a Multi-Root Workspace
Open/Close Workspace
What is a Workspace (VS Code)?
The workspace concept might be difficult to understand, mainly when you're getting started with VS Code. But, you'll see that it is a straightforward and useful concept to organize your projects. We'll try to keep this concept as simple as possible and applied to our ESP32 and ESP8266 projects. If you want to learn more, you can read the workspaces documentation.
A workspace is simply a collection of one or more project folders opened in a VS Code window (File Explorer).
Single Folder Worskapce
In most cases, you work with only one project folder at a timein this scenario, the workspace is your project's single root folder.
For example, create a new PlatformIO project or open a new project folder. In the following example, I created a new PlatformIO project called ESP32 Web Server. The workspace is the project folder itself.
Multi-Root Workspace
However, depending on the project, it might be useful to include multiple project folders in the workspace. For example, imagine that your project requires two or three ESP32 or ESP8266 boards that communicate with each other. In that case, it might be useful to have all the boards' project folders inside a single workspacethis is called Multi-root workspace.
For example, if you create another PlatformIO project, it will add it to the current workspace. In this case, the workspace name will be Untitled until you save it and give it a name. In this case, I created another project called ESP32 esp-now sender.
Advantages of Workspaces
[Multi-root workspace] It allows you to open/list on the File Explorer all folders inside that workspace without the need to open a single folder at a time.
A workspace is like a folder with extra capabilities. For example, a workspace can have configured settings that only applies to its folders (or to the folder itself, in the case of single-folder workspaces). Any other folder opened in VS Code outside that workspace won't have the same settings.
You can selectively enable or disable extensions for a specific workspace. This is useful if you interchangeably work with MicroPython or Arduino core projects. This allows you to automatically enable the right extensions for the project you're working on (PlatformIO for Arduino core projects, and PyMakr for MicroPython projects).
Adding Folders To Workspace
You can also add existing folders to the current workspace. You need to go to File > Add Folder to Workspace.. and select the project folder you want to add.
At this point, I have three projects under my untitled workspace.
Save Workspace
You can save your workspace and give it a name. To save your workspace, you need to go to File > Save Workspace As...
The workspace is saved as a file with the .code-workspace extension. In my case, I called my workspace file ESP-NOW-project.code-workspace.
Now, it shows up on the Explorer tab with the new name.
Workspace File
A workspace file has the .code-workspace extension, and it is simply a file that contains a list of the folders and files that should be listed on the File Explorer. Projects saved on the same workspace will show up simultaneously on VS Code File Explorer tab.
Additionally, the workspace file may also contain specific settings for the included folders, as we'll see in the next section.
To see how a workspace file looks like, go to File > Open File and select the .code-workspace file you've created when you saved the workspace.
As you can see, it includes the paths of the included project folders. At the moment, it doesn't have any custom settings yet. Let's see how to add custom settings to workspaces in the next section.
Worskpace Settings
As mentioned previously, a workspace is like a folder with extra capabilities. It can have configured settings that only apply to its folders (or to the folder itself, in single-folder workspaces).
To set custom settings for your workspace, you can go to File > Preferences > Settings. Then, select the workspace tab (this way, the defined setting will only apply to the currently opened workspace).
There are lots of settings that you can define for your workspace. For demonstration purposes, we've only made a few changes. Under the Commonly Used settings, we set the Auto Save to afterDelay and the font size to 12.
We also changed the Color Themego to Workbench > Appearance > Color Theme. We selected the Light + theme.
From now on, every time you open that workspace, it will show the light color theme.
Nonetheless, the default VS Code color theme remains the Dark +. This means that if you close the workspace or open another project, it will get back to the default settings or whatever settings you have defined for that specific workspace.
Now, if you open your workspace file, you'll see that it contains the new settings.
Workspace Enabled/Disabled Extensions
You can also enable or disable extensions for a specific workspace. For example, if you open a MicroPython project, you want the PyMakr extension to be enabled by default. However, you don't want that extension enabled if you're working on a project programmed with the Arduino core.
To show you how to do that, we'll enable the PyMakr extension for this specific workspace (imagine it contains several Micropython project folders).
You need to go to the Extensions tab and search for a specific extension. In our case, it's the PyMakr extension. Select that extension, and then under the Enable button, click on Enable (Workspace). For the changes to take effect, you need to restart VS Code.
From now on, that extension will be enabled when you're working on that workspace.
Uploading a Project in a Multi-Root Workspace
This section refers to PlaformIO projects (Arduino core).
When dealing with multi-root workspaces, you need to be able to choose which project you want to upload to your board. For that, you need to click on the current project name, as shown in the following image.
Then, a drop-down menu will show up at the top of the window with all the project folders on the workspace. You need to select the project folder you want to upload to your board.
Open/Close Workspave
To close the current workspace, you need to go to File > Close Workspace.
To open an existing workspace, go to File > Open Workspace
Wrapping Up
Using VS Code is one of the best choices for advanced (or even simple) ESP32 and ESP8266 projects. You can use VS Code to program your boards with the Arduino core using the PaltformIO extension, or with MicroPython firmware using the PyMakr extension. Both extensions were already covered in our blog (PlatformIO, PyMakr).
One of the biggest disadvantages of using VS Code is the learning curve. It may be tough for beginners to understand how everything works. One of the topics that confuse our readers a lot is the concept of Workspace. The workspace is simply a folder or collection of folders that show up on the File Explorer tab. The workspace can be configured with specific settings that apply to its folders.
After following this tutorial, we hope that you better understand what a workspace is and that you can take advantage of the workspace concept, mainly for projects that require multiple boards.
Let us know in the comments below if this is clear for you now.
with BMP388 Barometric/Altimeter Sensor (Arduino IDE)
In this guide, you'll learn how to use the BMP388 pressure sensor with the ESP32 board using Arduino IDE. The BMP388 is a tiny and precise absolute barometric pressure sensor. Because of its precision, it is often used to estimate altitude in drone applications. It can also be used in indoor/outdoor navigation, GPS applications, and others. The sensor communicates with a microcontroller using I2C or SPI communication protocols.
In this tutorial, we cover:
BMP388 Pinout
Wiring BMP388 with ESP32
Getting BMP388 Pressure, Altitude and Temperature with ESP32
ESP32 Web Server with BMP388
We have a similar tutorial for the ESP8266 board: ESP8266 NodeMCU with BMP388 Barometric/Altimeter Sensor (Arduino)
Introducing BMP388 Barometric Sensor
The BMP388 is a precise, low-power, low-noise absolute barometric pressure sensor that measures absolute pressure and temperature. Because pressure changes with altitude, we can also estimate altitude with great accuracy. For this reason, this sensor is handy for drone and navigation applications. You can also use it for other applications:
vertical velocity calculation;
internet of things;
weather forecast and weather stations;
health care applications;
fitness applications;
others
We're using the BMP388 sensor as a module, as shown in the figure below. It is also available in other different formats.
The following picture shows the other side of the sensor.
BMP388 Technical Data
The following table shows the key features of the BMP388 sensor. For more information, consult the datasheet.
Operation range |
300 to 1250 hPa (pressure)
-40 to +85oC (temperature) |
Interface |
I2C and SPI |
Average typical current consumption |
3.4 A @ 1Hz |
Absolute accuracy pressure (typ.)
P=900 1100 hPa (T=25 40C) |
0.5 hPa |
Relative accuracy pressure (typ.)
P=9001100 hPa (T=25 40C) |
0.08 hPa |
Noise in pressure (lowest bandwidth, highest resolution) |
0.03 Pa |
Maximum sampling rate |
200 Hz |
BMP388 Pinout
Here's the pinout of the BMP388 module we're usingit might be slightly different for other modules.
VIN |
Powers the sensor (5V) |
3V3 |
Powers the sensor (3V3) |
GND |
Common GND |
SCK |
SCL pin for I2C communication
SCK pin for SPI communication |
SDO |
SDO (MISO) pin for SPI communication |
SDI |
SDI (MOSI) pin for SPI communication
SDA pin for I2C communication |
CS |
Chip select pin for SPI communication |
INT |
Interrupt pin |
BMP388 Interface
As mentioned previously, the BMP388 sensor supports I2C and SPI interfaces.
BMP388 I2C
To use I2C communication protocol, use the following pins:
BMP388 |
ESP32 |
SDI (SDA) |
GPIO 21 |
SCK (SCL) |
GPIO 22 |
GPIO 22 (SCL) and GPIO 21 (SDA) are the default ESP32 I2C pins. You can use other pins as long as you set them properly in the code.
Recommended reading: ESP32 I2C Communication: Set Pins, Multiple Bus Interfaces and Peripherals (Arduino IDE)
BMP388 SPI
To use SPI communication protocol, use the following pins:
BMP388 |
ESP32 |
SCK |
GPIO 18 |
SDI (MOSI) |
GPIO 23 |
SDO (MISO) |
GPIO 19 |
CS (Chip Select) |
GPIO 5 |
These are the default ESP32 SPI pins. You can use other pins as long as you set them properly in the code.
Parts Required
To complete this tutorial you need the following parts:
BMP388 sensor module
ESP32 (read Best ESP32 development boards)
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic ESP32 with BMP388
The BMP388 can communicate using I2C or SPI communication protocols.
ESP32 with BMP388 using I2C
Follow the next schematic diagram to wire the BMP388 to the ESP32 using the default I2C pins.
ESP32 with BMP388 using SPI
Alternatively, you may want to use the SPI communication protocol instead. In that case, follow the next schematic diagram to wire the BMP388 to the ESP32 using the default SPI pins.
Preparing Arduino IDE
We'll program the ESP32 board using Arduino IDE. So, make sure you have the ESP32 add-on installed. Follow the next tutorial:
Install the ESP32 Board in Arduino IDE
If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP32:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
Installing the Adafruit BMP3XX Library
There are different libraries compatible with the BMP388 sensor and the ESP32. In this tutorial, we'll use the Adafruit BMP3XX library.
Follow the next steps to install the library in your Arduino IDE:
Open your Arduino IDE and go to Sketch > Include Library > Manage Libraries. The Library Manager should open.
Search for adafruit bmp3xx in the search box and install the library.
Installing the Adafruit_Sensor Library
To use the BMP3XX library, you also need to install the Adafruit_Sensor library. Follow the next steps to install the library in your Arduino IDE:
Go to Sketch > Include Library > Manage Libraries and type Adafruit Unified Sensor in the search box. Scroll all the way down to find the library and install it.
After installing the libraries, restart your Arduino IDE.
Code Reading BMP388 Pressure, Altitude and Temperature
The best way to get familiar with a new sensor is to start with a basic example provided by the library.
After installing the BMP3XX library, open the Arduino IDE and go to File > Examples > Adafruit BMP3XX Library > bmp3XX_simpletest. We've made a few changes to make it fully compatible with the ESP32.
/***************************************************************************
This is a library for the BMP3XX temperature & pressure sensor. Designed specifically to work with the Adafruit BMP388 Breakout ----> http://www.adafruit.com/products/3966
These sensors use I2C or SPI to communicate, 2 or 4 pins are required to interface. Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit!
Written by Limor Fried & Kevin Townsend for Adafruit Industries. BSD license, all text above must be included in any redistribution
***************************************************************************/
// Complete project details: https://RandomNerdTutorials.com/esp32-bmp388-arduino/
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BMP3XX.h"
#define BMP_SCK 18
#define BMP_MISO 19
#define BMP_MOSI 23
#define BMP_CS 5
#define SEALEVELPRESSURE_HPA (1013.25)
Adafruit_BMP3XX bmp;
void setup() {
Serial.begin(115200);
while (!Serial);
Serial.println("Adafruit BMP388 / BMP390 test");
if (!bmp.begin_I2C()) { // hardware I2C mode, can pass in address & alt Wire
//if (! bmp.begin_SPI(BMP_CS)) { // hardware SPI mode
//if (! bmp.begin_SPI(BMP_CS, BMP_SCK, BMP_MISO, BMP_MOSI)) { // software SPI mode
Serial.println("Could not find a valid BMP3 sensor, check wiring!");
while (1);
}
// Set up oversampling and filter initialization
bmp.setTemperatureOversampling(BMP3_OVERSAMPLING_8X);
bmp.setPressureOversampling(BMP3_OVERSAMPLING_4X);
bmp.setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3);
bmp.setOutputDataRate(BMP3_ODR_50_HZ);
}
void loop() {
if (! bmp.performReading()) {
Serial.println("Failed to perform reading :(");
return;
}
Serial.print("Temperature = ");
Serial.print(bmp.temperature);
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bmp.pressure / 100.0);
Serial.println(" hPa");
Serial.print("Approx. Altitude = ");
Serial.print(bmp.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(" m");
Serial.println();
delay(2000);
}
View raw code
Sea Level Pressure
To get more accurate results for pressure and altitude, we recommend that you adjust the sea level pressure for your location in the SEALEVELPRESSURE_HPA variable:
#define SEALEVELPRESSURE_HPA (1013.25)
The standard value is 1013.25 hPa. For more accurate results, check the sea level pressure at your location. You can use this website to check sea level pressure.
How the Code Works
Continue reading this section to learn how the code works, or skip to the Demonstration section.
Libraries
The code starts by including the needed libraries: the Wire library to use I2C, the SPI library (if you want to use SPI instead of I2C), the Adafruit_Sensor, and Adafruit_BMP3XX libraries to interface with the BMP388 sensor.
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BMP3XX.h"
SPI communication
We prefer to use the I2C communication protocol with the sensor. However, the code is prepared if you want to use SPI. The following lines of code define the SPI pins.
#define BMP_SCK 18
#define BMP_MISO 19
#define BMP_MOSI 23
#define BMP_CS 5
Sea level pressure
A variable called SEALEVELPRESSURE_HPA is created.
#define SEALEVELPRESSURE_HPA (1013.25)
This variable saves the pressure at the sea level in hectopascal (is equivalent to milibar). This variable is used to estimate the altitude for a given pressure by comparing it with the sea level pressure. This example uses the default value, but for accurate results, replace the value with the current sea level pressure at your location. You can use this website to check sea level pressure.
setup()
In the setup() start a serial communication.
Serial.begin(115200);
Init BMP388 Sensor I2C
This example uses I2C communication protocol by default. The following line starts an Adafruit_BMP3XX object called bmp on the default ESP32 I2C pins: GPIO 22 (SCL), GPIO 21 (SDA).
if (!bmp.begin_I2C()) { // hardware I2C mode, can pass in address & alt Wire
To use SPI, you need to comment this previous line and uncomment one of the following lines for hardware SPI (use the default SPI pins and choose the CS pin) or software SPI (use any pins).
//if (! bmp.begin_SPI(BMP_CS)) { // hardware SPI mode
//if (! bmp.begin_SPI(BMP_CS, BMP_SCK, BMP_MISO, BMP_MOSI)) { // software SPI mode
Set up the following parameters (oversampling and filter) for the sensor.
// Set up oversampling and filter initialization
bmp.setTemperatureOversampling(BMP3_OVERSAMPLING_8X);
bmp.setPressureOversampling(BMP3_OVERSAMPLING_4X);
bmp.setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3);
bmp.setOutputDataRate(BMP3_ODR_50_HZ);
To increase the resolution of the raw sensor data, it supports oversampling. We'll use the default oversampling parameters, but you can change them.
setTemperatureOversampling(): set temperature oversampling.
setPressureOversampling(): set pressure oversampling.
These methods can accept one of the following parameters:
BMP3_NO_OVERSAMPLING
BMP3_OVERSAMPLING_2X
BMP3_OVERSAMPLING_4X
BMP3_OVERSAMPLING_8X
BMP3_OVERSAMPLING_16X
BMP3_OVERSAMPLING_32X
The setIIRFilterCoeff() function sets the coefficient of the filter (in samples). It can be:
BMP3_IIR_FILTER_DISABLE (no filtering)
BMP3_IIR_FILTER_COEFF_1
BMP3_IIR_FILTER_COEFF_3
BMP3_IIR_FILTER_COEFF_7
BMP3_IIR_FILTER_COEFF_15
BMP3_IIR_FILTER_COEFF_31
BMP3_IIR_FILTER_COEFF_63
BMP3_IIR_FILTER_COEFF_127
Set the output data rate with the setOutputDataRate() function. It can accept one of the following options:
BMP3_ODR_200_HZ, BMP3_ODR_100_HZ, BMP3_ODR_50_HZ, BMP3_ODR_25_HZ,BMP3_ODR_12_5_HZ, BMP3_ODR_6_25_HZ, BMP3_ODR_3_1_HZ, BMP3_ODR_1_5_HZ, BMP3_ODR_0_78_HZ, BMP3_ODR_0_39_HZ,BMP3_ODR_0_2_HZ, BMP3_ODR_0_1_HZ, BMP3_ODR_0_05_HZ, BMP3_ODR_0_02_HZ, BMP3_ODR_0_01_HZ, BMP3_ODR_0_006_HZ, BMP3_ODR_0_003_HZ, or BMP3_ODR_0_001_HZ
loop()
In the loop(), we'll get measurements from the BMP388 sensor. First, tell the sensor to get new readings with bmp.performReading().
if (! bmp.performReading()) {
Serial.println("Failed to perform reading :(");
return;
}
Then, get and print the temperature, pressure and altitude readings as follows:
Serial.print("Temperature = ");
Serial.print(bmp.temperature);
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bmp.pressure / 100.0);
Serial.println(" hPa");
Serial.print("Approx. Altitude = ");
Serial.print(bmp.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(" m");
You get each specific reading as follows:
bmp.temperature: returns temperature reading
bmp.pressure: returns pressure reading
bmp.readAltitude (SEALEVELPRESSURE_HPA): returns altitude estimation
Demonstration
After inserting the sea level pressure for your location, you can upload the code to your board. In your Arduino IDE, go to Tools > Boards and select the board you're using. Then, in Tools > Port, select the COM port.
After uploading, open the Serial Monitor at a baud rate of 115200. The readings will be printed in the Serial Monitor.
Notice that if you increase the sensor's altitude, it will be reflected in the altitude reading. The altitude estimation is pretty accurate. It can detect small changes in the centimeters or inches range. You can check it by comparing the altitude you're getting with the altitude of your location. To get your location's altitude, you can use this website.
ESP32 Web Server with BMP388
In this section, we provide an example of web server that you can build with the ESP32 to display BMP388 readings on a web page.
Installing Libraries Async Web Server
To build the web server you need to install the following libraries. Click the links below to download the libraries.
ESPAsyncWebServer
AsyncTCP
These libraries aren't available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch > Include Library > Add .zip Library and select the libraries you've just downloaded.
Code
Then, upload the following code to your board (type your SSID and password).
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-bmp388-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP3XX.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
//Uncomment if using SPI
/*#define BMP_SCK 18
#define BMP_MISO 19
#define BMP_MOSI 23
#define BMP_CS 5*/
#define SEALEVELPRESSURE_HPA (1013.25)
Adafruit_BMP3XX bmp;
float temp;
float pres;
float alt;
AsyncWebServer server(80);
AsyncEventSource events("/events");
unsigned long lastTime = 0;
unsigned long timerDelay = 30000; // send readings timer
void getBMPReadings(){
if (! bmp.performReading()) {
Serial.println("Failed to perform reading :(");
return;
}
temp = bmp.temperature;
pres = bmp.pressure / 100.0;
alt = bmp.readAltitude(SEALEVELPRESSURE_HPA);
}
String processor(const String& var){
getBMPReadings();
//Serial.println(var);
if(var == "TEMPERATURE"){
return String(temp);
}
else if(var == "PRESSURE"){
return String(pres);
}
else if(var == "ALTITUDE"){
return String(alt);
}
}
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>BMP388 Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<link rel="icon" href="data:,">
<style>
html {font-family: Arial; display: inline-block; text-align: center;}
p { font-size: 1.2rem;}
body { margin: 0;}
.topnav { overflow: hidden; background-color: #0F7173; color: white; font-size: 1.4rem; }
.content { padding: 20px; }
.card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }
.cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.reading { font-size: 2rem; }
.card.temperature { color: #272932; }
.card.altitude { color: #D8A47F; }
.card.pressure { color: #F05D5E; }
</style>
</head>
<body>
<div>
<h3>BMP388 WEB SERVER</h3>
</div>
<div>
<div>
<div>
<h4><i></i> PRESSURE</h4><p><span><span>%PRESSURE%</span> hPa</span></p>
</div>
<div>
<h4><i></i> ALTITUDE</h4><p><span><span>%ALTITUDE%</span> m</span></p>
</div>
<div>
<h4><i></i> TEMPERATURE</h4><p><span><span>%TEMPERATURE%</span> °C</span></p>
</div>
</div>
</div>
<script>
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('message', function(e) {
console.log("message", e.data);
}, false);
source.addEventListener('temperature', function(e) {
console.log("temperature", e.data);
document.getElementById("temp").innerHTML = e.data;
}, false);
source.addEventListener('pressure', function(e) {
console.log("pressure", e.data);
document.getElementById("pres").innerHTML = e.data;
}, false);
source.addEventListener('altitude', function(e) {
console.log("altitude", e.data);
document.getElementById("alt").innerHTML = e.data;
}, false);
}
</script>
</body>
</html>)rawliteral";
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
// Set device as a Wi-Fi Station
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Setting as a Wi-Fi Station..");
}
Serial.print("Station IP Address: ");
Serial.println(WiFi.localIP());
Serial.println();
// Init BMEP388 sensor
if (!bmp.begin_I2C()) { // hardware I2C mode, can pass in address & alt Wire
//if (! bmp.begin_SPI(BMP_CS)) { // hardware SPI mode
//if (! bmp.begin_SPI(BMP_CS, BMP_SCK, BMP_MISO, BMP_MOSI)) { // software SPI mode
Serial.println("Could not find a valid BMP3 sensor, check wiring!");
while (1);
}
// Set up oversampling and filter initialization
bmp.setTemperatureOversampling(BMP3_OVERSAMPLING_8X);
bmp.setPressureOversampling(BMP3_OVERSAMPLING_4X);
bmp.setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3);
bmp.setOutputDataRate(BMP3_ODR_50_HZ);
//Get readings when initializing
getBMPReadings();
// Handle Web Server
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
// Handle Web Server Events
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
// send event with message "hello!", id current millis
// and set reconnect delay to 1 second
client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);
server.begin();
}
void loop() {
if ((millis() - lastTime) > timerDelay) {
getBMPReadings();
Serial.printf("Pressure = %.2f hPa \n", pres);
Serial.printf("Altitude = %.2f m \n", alt);
Serial.printf("Temperature = %.2f oC \n", temp);
Serial.println();
// Send Events to the Web Server with the Sensor Readings
events.send("ping",NULL,millis());
events.send(String(pres).c_str(),"pressure",millis());
events.send(String(alt).c_str(),"altitude",millis());
events.send(String(temp).c_str(),"temperature",millis());
lastTime = millis();
}
}
View raw code
Demonstration
After uploading, open the Serial Monitor at a baud rate of 115200 to get the ESP32 IP address.
Open a browser and type the IP address. You should get access to the web server with the latest sensor readings. You can access the web server on your computer, tablet, or smartphone in your local network.
The readings are updated automatically on the web server using Server-Sent Events. You can check the Server-Sent Events tutorial to have an idea of how it works.
Wrapping Up
The BMP388 is a small and very precise pressure sensor that allows you to estimate altitude with great precision. The sensor also measures temperature. It is great for outdoor/indoor navigation, drones, weather stations, and other applications.
You've learned how to use the sensor with the ESP32 development board and Arduino IDE in this tutorial. We hope you found this getting started guide useful.
ESP32-CAM Remote Controlled Car Robot Web Server
Build a Wi-Fi remote controlled car robot with the ESP32-CAM. You'll be able to control the robot using a web server that displays a video streaming of what the robot sees. You can control your robot remotely even if it's out of your sight. The ESP32-CAM will be programmed using Arduino IDE.
Boards compatibility: this project requires 4 GPIOs to control the DC motors. So, you can use any ESP32 camera board with 4 available GPIOs like the ESP32-CAM Ai-Thinker board or the TTGO T-Journal.
Project Overview
Before starting the project, we'll highlight the most important features and components used to build the robot.
Wi-Fi
The robot will be controlled via Wi-Fi using your ESP32-CAM. We'll create a web-based interface to control the robot, that can be accessed in any device inside your local network.
The web page also shows a video streaming of what the robot sees. For good results with video streaming, we recommend using an ESP32-CAM with external antenna.
Important: without an external antenna the video stream lags and the web server is extremely slow to control the robot.
Robot Controls
The web server has 5 controls: Forward, Backward, Left, Right, and Stop.
The robot moves as long as you're pressing the buttons. When you release any button, the robot stops. However, we've included the Stop button that can be useful in case the ESP32 doesn't receive the stop command when you release a button.
Smart Robot Chassis Kit
We're going to use the Smart Robot Chassis Kit. You can find it in most online stores. The kit costs around $10 and it's easy to assemble watch this video to see how to assemble the robot chassis kit.
You can use any other chassis kit as long as it comes with two DC motors.
L298N Motor Driver
There are many ways to control DC motors. We'll use the L298N motor driver that provides an easy way to control the speed and direction of 2 DC motors.
We won't explain how the L298N motor driver works. You can read the following article for an in-depth tutorial about the L298N motor driver:
ESP32 with DC Motor and L298N Motor Driver Control Speed and Direction
Power
To keep the circuitry simple, we'll power the robot (motors) and the ESP32 using the same power source. We used a power bank/portable charger (like the ones used to charge your smartphone) and it worked well.
Note: the motors draw a lot of current, so if you feel your robot is not moving properly, you may need to use an external power supply for the motors. This means you need two different power sources. One to power the DC motors, and the other to power the ESP32.
Parts Required
For this project, we'll use the following parts:
ESP32-CAM AI-Thinker with external antenna
L298N Motor Driver
Robot Car Chassis Kit
Power bank or other 5V power supply
Prototyping circuit board (optional)
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Code
Copy the following code to your Arduino IDE.
/*********
Rui Santos
Complete instructions at https://RandomNerdTutorials.com/esp32-cam-projects-ebook/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
#include "esp_camera.h"
#include <WiFi.h>
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h" // disable brownout problems
#include "soc/rtc_cntl_reg.h" // disable brownout problems
#include "esp_http_server.h"
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
#define PART_BOUNDARY "123456789000000000000987654321"
#define CAMERA_MODEL_AI_THINKER
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WITHOUT_PSRAM
//#define CAMERA_MODEL_M5STACK_PSRAM_B
//#define CAMERA_MODEL_WROVER_KIT
#if defined(CAMERA_MODEL_WROVER_KIT)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 21
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 19
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 5
#define Y2_GPIO_NUM 4
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#elif defined(CAMERA_MODEL_M5STACK_PSRAM)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM 27
#define SIOD_GPIO_NUM 25
#define SIOC_GPIO_NUM 23
#define Y9_GPIO_NUM 19
#define Y8_GPIO_NUM 36
#define Y7_GPIO_NUM 18
#define Y6_GPIO_NUM 39
#define Y5_GPIO_NUM 5
#define Y4_GPIO_NUM 34
#define Y3_GPIO_NUM 35
#define Y2_GPIO_NUM 32
#define VSYNC_GPIO_NUM 22
#define HREF_GPIO_NUM 26
#define PCLK_GPIO_NUM 21
#elif defined(CAMERA_MODEL_M5STACK_WITHOUT_PSRAM)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM 27
#define SIOD_GPIO_NUM 25
#define SIOC_GPIO_NUM 23
#define Y9_GPIO_NUM 19
#define Y8_GPIO_NUM 36
#define Y7_GPIO_NUM 18
#define Y6_GPIO_NUM 39
#define Y5_GPIO_NUM 5
#define Y4_GPIO_NUM 34
#define Y3_GPIO_NUM 35
#define Y2_GPIO_NUM 17
#define VSYNC_GPIO_NUM 22
#define HREF_GPIO_NUM 26
#define PCLK_GPIO_NUM 21
#elif defined(CAMERA_MODEL_AI_THINKER)
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#elif defined(CAMERA_MODEL_M5STACK_PSRAM_B)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM 27
#define SIOD_GPIO_NUM 22
#define SIOC_GPIO_NUM 23
#define Y9_GPIO_NUM 19
#define Y8_GPIO_NUM 36
#define Y7_GPIO_NUM 18
#define Y6_GPIO_NUM 39
#define Y5_GPIO_NUM 5
#define Y4_GPIO_NUM 34
#define Y3_GPIO_NUM 35
#define Y2_GPIO_NUM 32
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 26
#define PCLK_GPIO_NUM 21
#else
#error "Camera model not selected"
#endif
#define MOTOR_1_PIN_1 14
#define MOTOR_1_PIN_2 15
#define MOTOR_2_PIN_1 13
#define MOTOR_2_PIN_2 12
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
httpd_handle_t camera_httpd = NULL;
httpd_handle_t stream_httpd = NULL;
static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<html>
<head>
<title>ESP32-CAM Robot</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; text-align: center; margin:0px auto; padding-top: 30px;}
table { margin-left: auto; margin-right: auto; }
td { padding: 8 px; }
.button {
background-color: #2f4468;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 18px;
margin: 6px 3px;
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
img { width: auto ;
max-width: 100% ;
height: auto ;
}
</style>
</head>
<body>
<h1>ESP32-CAM Robot</h2>
<img src="" >
<table>
<tr><td colspan="3" align="center"><button onmousedown="toggleCheckbox('forward');" ontouchstart="toggleCheckbox('forward');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Forward</button></td></tr>
<tr><td align="center"><button onmousedown="toggleCheckbox('left');" ontouchstart="toggleCheckbox('left');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Left</button></td><td align="center"><button onmousedown="toggleCheckbox('stop');" ontouchstart="toggleCheckbox('stop');">Stop</button></td><td align="center"><button onmousedown="toggleCheckbox('right');" ontouchstart="toggleCheckbox('right');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Right</button></td></tr>
<tr><td colspan="3" align="center"><button onmousedown="toggleCheckbox('backward');" ontouchstart="toggleCheckbox('backward');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Backward</button></td></tr>
</table>
<script>
function toggleCheckbox(x) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/action?go=" + x, true);
xhr.send();
}
window.onload = document.getElementById("photo").src = window.location.href.slice(0, -1) + ":81/stream";
</script>
</body>
</html>
)rawliteral";
static esp_err_t index_handler(httpd_req_t *req){
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, (const char *)INDEX_HTML, strlen(INDEX_HTML));
}
static esp_err_t stream_handler(httpd_req_t *req){
camera_fb_t * fb = NULL;
esp_err_t res = ESP_OK;
size_t _jpg_buf_len = 0;
uint8_t * _jpg_buf = NULL;
char * part_buf[64];
res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
if(res != ESP_OK){
return res;
}
while(true){
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
res = ESP_FAIL;
} else {
if(fb->width > 400){
if(fb->format != PIXFORMAT_JPEG){
bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
esp_camera_fb_return(fb);
fb = NULL;
if(!jpeg_converted){
Serial.println("JPEG compression failed");
res = ESP_FAIL;
}
} else {
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
}
}
}
if(res == ESP_OK){
size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
}
if(res == ESP_OK){
res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
}
if(res == ESP_OK){
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
}
if(fb){
esp_camera_fb_return(fb);
fb = NULL;
_jpg_buf = NULL;
} else if(_jpg_buf){
free(_jpg_buf);
_jpg_buf = NULL;
}
if(res != ESP_OK){
break;
}
//Serial.printf("MJPG: %uB\n",(uint32_t)(_jpg_buf_len));
}
return res;
}
static esp_err_t cmd_handler(httpd_req_t *req){
char* buf;
size_t buf_len;
char variable[32] = {0,};
buf_len = httpd_req_get_url_query_len(req) + 1;
if (buf_len > 1) {
buf = (char*)malloc(buf_len);
if(!buf){
httpd_resp_send_500(req);
return ESP_FAIL;
}
if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
if (httpd_query_key_value(buf, "go", variable, sizeof(variable)) == ESP_OK) {
} else {
free(buf);
httpd_resp_send_404(req);
return ESP_FAIL;
}
} else {
free(buf);
httpd_resp_send_404(req);
return ESP_FAIL;
}
free(buf);
} else {
httpd_resp_send_404(req);
return ESP_FAIL;
}
sensor_t * s = esp_camera_sensor_get();
int res = 0;
if(!strcmp(variable, "forward")) {
Serial.println("Forward");
digitalWrite(MOTOR_1_PIN_1, 1);
digitalWrite(MOTOR_1_PIN_2, 0);
digitalWrite(MOTOR_2_PIN_1, 1);
digitalWrite(MOTOR_2_PIN_2, 0);
}
else if(!strcmp(variable, "left")) {
Serial.println("Left");
digitalWrite(MOTOR_1_PIN_1, 0);
digitalWrite(MOTOR_1_PIN_2, 1);
digitalWrite(MOTOR_2_PIN_1, 1);
digitalWrite(MOTOR_2_PIN_2, 0);
}
else if(!strcmp(variable, "right")) {
Serial.println("Right");
digitalWrite(MOTOR_1_PIN_1, 1);
digitalWrite(MOTOR_1_PIN_2, 0);
digitalWrite(MOTOR_2_PIN_1, 0);
digitalWrite(MOTOR_2_PIN_2, 1);
}
else if(!strcmp(variable, "backward")) {
Serial.println("Backward");
digitalWrite(MOTOR_1_PIN_1, 0);
digitalWrite(MOTOR_1_PIN_2, 1);
digitalWrite(MOTOR_2_PIN_1, 0);
digitalWrite(MOTOR_2_PIN_2, 1);
}
else if(!strcmp(variable, "stop")) {
Serial.println("Stop");
digitalWrite(MOTOR_1_PIN_1, 0);
digitalWrite(MOTOR_1_PIN_2, 0);
digitalWrite(MOTOR_2_PIN_1, 0);
digitalWrite(MOTOR_2_PIN_2, 0);
}
else {
res = -1;
}
if(res){
return httpd_resp_send_500(req);
}
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
return httpd_resp_send(req, NULL, 0);
}
void startCameraServer(){
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 80;
httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = index_handler,
.user_ctx = NULL
};
httpd_uri_t cmd_uri = {
.uri = "/action",
.method = HTTP_GET,
.handler = cmd_handler,
.user_ctx = NULL
};
httpd_uri_t stream_uri = {
.uri = "/stream",
.method = HTTP_GET,
.handler = stream_handler,
.user_ctx = NULL
};
if (httpd_start(&camera_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(camera_httpd, &index_uri);
httpd_register_uri_handler(camera_httpd, &cmd_uri);
}
config.server_port += 1;
config.ctrl_port += 1;
if (httpd_start(&stream_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(stream_httpd, &stream_uri);
}
}
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
pinMode(MOTOR_1_PIN_1, OUTPUT);
pinMode(MOTOR_1_PIN_2, OUTPUT);
pinMode(MOTOR_2_PIN_1, OUTPUT);
pinMode(MOTOR_2_PIN_2, OUTPUT);
Serial.begin(115200);
Serial.setDebugOutput(false);
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
if(psramFound()){
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 10;
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
}
// Camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
// Wi-Fi connection
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("Camera Stream Ready! Go to: http://");
Serial.println(WiFi.localIP());
// Start streaming web server
startCameraServer();
}
void loop() {
}
View raw code
Insert your network credentials and the code should work straight away.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
How the Code Works
Let's take a look at the relevant parts to control the robot. Define the GPIOs that will control the motors. Each motor is controlled by two pins.
#define MOTOR_1_PIN_1 14
#define MOTOR_1_PIN_2 15
#define MOTOR_2_PIN_1 13
#define MOTOR_2_PIN_2 12
When you click the buttons, you make a request on a different URL.
<table>
<tr><td colspan="3" align="center"><button onmousedown="toggleCheckbox('forward');" ontouchstart="toggleCheckbox('forward');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Forward</button></td></tr>
<tr><td align="center"><button onmousedown="toggleCheckbox('left');" ontouchstart="toggleCheckbox('left');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Left</button></td><td align="center"><button onmousedown="toggleCheckbox('stop');" ontouchstart="toggleCheckbox('stop');">Stop</button></td><td align="center"><button onmousedown="toggleCheckbox('right');" ontouchstart="toggleCheckbox('right');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Right</button></td></tr>
<tr><td colspan="3" align="center"><button onmousedown="toggleCheckbox('backward');" ontouchstart="toggleCheckbox('backward');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Backward</button></td></tr>
</table>
<script>
function toggleCheckbox(x) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/action?go=" + x, true);
xhr.send();
}
window.onload = document.getElementById("photo").src = window.location.href.slice(0, -1) + ":81/stream";
</script>
Here's the requests made depending on the button that is being pressed:
Forward:
<ESP_IP_ADDRESS>/action?go=forward
Backward:
/action?go=backward
Left:
/action?go=left
Right:
/action?go=right
Stop:
/action?go=stop
When you release the button, a request is made on the /action?go=stop URL. The robot only moves as long as you're pressing the buttons.
Handle Requests
To handle what happens when we get requests on those URLs, we use these if else statements:
if(!strcmp(variable, "forward")) {
Serial.println("Forward");
digitalWrite(MOTOR_1_PIN_1, 1);
digitalWrite(MOTOR_1_PIN_2, 0);
digitalWrite(MOTOR_2_PIN_1, 1);
digitalWrite(MOTOR_2_PIN_2, 0);
}
else if(!strcmp(variable, "left")) {
Serial.println("Left");
digitalWrite(MOTOR_1_PIN_1, 0);
digitalWrite(MOTOR_1_PIN_2, 1);
digitalWrite(MOTOR_2_PIN_1, 1);
digitalWrite(MOTOR_2_PIN_2, 0);
}
else if(!strcmp(variable, "right")) {
Serial.println("Right");
digitalWrite(MOTOR_1_PIN_1, 1);
digitalWrite(MOTOR_1_PIN_2, 0);
digitalWrite(MOTOR_2_PIN_1, 0);
digitalWrite(MOTOR_2_PIN_2, 1);
}
else if(!strcmp(variable, "backward")) {
Serial.println("Backward");
digitalWrite(MOTOR_1_PIN_1, 0);
digitalWrite(MOTOR_1_PIN_2, 1);
digitalWrite(MOTOR_2_PIN_1, 0);
digitalWrite(MOTOR_2_PIN_2, 1);
}
else if(!strcmp(variable, "stop")) {
Serial.println("Stop");
digitalWrite(MOTOR_1_PIN_1, 0);
digitalWrite(MOTOR_1_PIN_2, 0);
digitalWrite(MOTOR_2_PIN_1, 0);
digitalWrite(MOTOR_2_PIN_2, 0);
}
Testing the Code
After inserting your network credentials, you can upload the code to your ESP32-CAM board. If you don't know how to upload code to the board, follow the next tutorial:
How to Program / Upload Code to ESP32-CAM AI-Thinker (Arduino IDE)
After uploading, open the Serial Monitor to get its IP address.
Open a browser and type the ESP IP address. A similar web page should load:
Press the buttons and take a look at the Serial Monitor to see if it is streaming without lag and if it is receiving the commands without crashing.
If everything is working properly, it's time to assemble the circuit.
Circuit
After assembling the robot chassis, you can wire the circuit by following the next schematic diagram.
Start by connecting the ESP32-CAM to the motor driver as shown in the schematic diagram. You can either use a mini breadboard or a stripboard to place your ESP32-CAM and build the circuit.
The following table shows the connections between the ESP32-CAM and the L298N Motor Driver.
L298N Motor Driver |
ESP32-CAM |
IN1 |
GPIO 14 |
IN2 |
GPIO 15 |
IN3 |
GPIO 13 |
IN4 |
GPIO 12 |
We assembled all the connections on a mini stripboard as shown below.
After that, wire each motor to its terminal block.
Note: we suggest soldering a 0.1 uF ceramic capacitor to the positive and negative terminals of each motor, as shown in the diagram to help smooth out any voltage spikes. Additionally, you can solder a slider switch to the red wire that comes from the power bank. This way, you can turn the power on and off.
Finally, apply power with a power bank as shown in the schematic diagram. You need to strip a USB cable. In this example, the ESP32-CAM and the motors are being powered using the same power source and it works well.
Note: the motors draw a lot of current, so if you feel your robot is not moving fast enough, you may need to use an external power supply for the motors. This means you need two different power sources. One to power the DC motors, and the other to power the ESP32. You can use a 4 AA battery pack to power the motors. When you get your robot chassis kit, you usually get a battery holder for 4 AA batteries.
Your robot should look similar to the following figure:
Don't forget that you should use an external antenna with the ESP32-CAM, otherwise the web server might be extremely slow.
Demonstration
Open a browser on the ESP32-CAM IP address, and you should be able to control your robot. The web server works well on a laptop computer or smartphone.
You can only have the web server open in one device/tab at a time.
Wrapping Up
In this tutorial you've learned how to build a remote controlled robot using the ESP32-CAM and how to control it using a web server.
Controlling DC motors with the ESP32-CAM is the same as controlling them using a regular ESP32. Read this tutorial to learn more: ESP32 with DC Motor and L298N Motor Driver Control Speed and Direction.
If you want to control your robot outside the range of your local network, you might consider setting the ESP32-CAM as an access point. This way, the ESP32-CAM doesn't need to connect to your router, it creates its own wi-fi network and nearby wi-fi devices like your smartphone can connect to it.
Publish Sensor Readings to ThingSpeak (easiest way)
In this guide, you'll learn how to send sensor readings with the ESP32 to ThingSpeak. For demonstration purposes, we'll use a BME280 sensor, but you can easily modify the examples to use any other sensor. The ESP32 board will be programmed using the Arduino core.
ThingSpeak allows you to publish your sensor readings to their website and plot them in charts with timestamps. Then, you can access your readings from anywhere in the world.
We have a similar tutorial for the ESP8266 board: ESP8266 NodeMCU Publish Sensor Readings to ThingSpeak (easiest way)
Project Overview
There are many ways to send sensor readings to ThingSpeak. In this tutorial, we'll use one of the easiest waysusing the thingspeak-arduino library. This library provides methods to easily publish sensor readings to single fields or multiple fields. You can check the library examples on its GitHub page.
To exemplify, we'll use the BME280 sensor, but you can use any other sensor (you need to modify the code).
Recommended reading: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity).
We'll cover how to publish to a single field and how to publish to multiple fields.
Preparing Arduino IDE
For this tutorial we'll program the ESP32 using the Arduino core. So, make sure you have the ESP32 add-on installed in your Arduino IDE:
Installing the ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
If you want to program the ESP32 using VS Code with the PlatformIO extension, follow the next tutorial instead:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266
Installing the ThingSpeak Library
To send sensor readings to ThingSpeak, we'll use the thingspeak-arduino library. You can install this library through the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for ThingSpeak in the Library Manager. Install the ThingSpeak library by MathWorks.
Installing BME280 Libraries
As mentioned previously, we'll publish sensor readings from a BME280 sensor. So, you also need to install the libraries to interface with the BME280 sensor.
Adafruit_BME280 library
Adafruit_Sensor library
You can install the libraries using the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the library name.
Installing Libraries (VS Code + PlatformIO)
If you're using VS Code with the PlatformIO extension, copy the following to the platformio.ini file to include the libraries.
lib_deps = mathworks/ThingSpeak@^2.0.0
adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/Adafruit BME280 Library @ ^2.1.2
Building the Circuit
To exemplify how to send data to ThingSpeak, we'll send sensor readings from a BME280 sensor. So, you need to wire a BME280 sensor to your ESP32.
Parts Required
To complete this tutorial you need the following parts:
BME280 sensor module
ESP32 (read Best ESP32 development boards)
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic Diagram
We're going to use I2C communication with the BME280 sensor module. For that, wire the sensor to the default ESP32 SCL (GPIO 22) and SDA (GPIO 21) pins, as shown in the following schematic diagram.
Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
ThingSpeak Getting Started
Go to ThingSpeak an click the Get Started For Free button to create a new account. This account is linked to a Mathworks account. So, if you already have a Mathworks account, you should log in with that account.
Creating New Channel
After your account is ready, sign in, open the Channels tab and select My Channels.
Press the New Channel button to create a new channel.
Type a name for your channel and add a description. In this example, we'll just publish temperature. If you want to publish multiple readings (like humidity and pressure), you can enable more fields as you'll see later in this tutorial.
Click the Save Channel button to create and save your channel.
Customizing Chart
The chart can be customized, go to your Private View tab and click on the edit icon.
You can give a title to your chart, customize the background color, x and y axis, and much more.
When you're done, press the Save button.
API Key
To send values from the ESP32 to ThingSpeak, you need the Write API Key. Open the API Keys
tab and copy the Write API Key to a safe place because you'll need it in a moment.
ESP32 Publish Sensor Readings to ThingSpeak Code
Copy the following code to your Arduino IDE (or to the main.cpp file if you're using PlatformIO).
/*
Adapted from WriteSingleField Example from ThingSpeak Library (Mathworks)
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-thingspeak-publish-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
#include "ThingSpeak.h"
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>
const char* ssid = "REPLACE_WITH_YOUR_SSID"; // your network SSID (name)
const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // your network password
WiFiClient client;
unsigned long myChannelNumber = X;
const char * myWriteAPIKey = "XXXXXXXXXXXXXXXX";
// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
// Variable to hold temperature readings
float temperatureC;
//uncomment if you want to get temperature in Fahrenheit
//float temperatureF;
// Create a sensor object
Adafruit_BME280 bme; //BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
void setup() {
Serial.begin(115200); //Initialize serial
initBME();
WiFi.mode(WIFI_STA);
ThingSpeak.begin(client); // Initialize ThingSpeak
}
void loop() {
if ((millis() - lastTime) > timerDelay) {
// Connect or reconnect to WiFi
if(WiFi.status() != WL_CONNECTED){
Serial.print("Attempting to connect");
while(WiFi.status() != WL_CONNECTED){
WiFi.begin(ssid, password);
delay(5000);
}
Serial.println("\nConnected.");
}
// Get a new temperature reading
temperatureC = bme.readTemperature();
Serial.print("Temperature (oC): ");
Serial.println(temperatureC);
//uncomment if you want to get temperature in Fahrenheit
/*temperatureF = 1.8 * bme.readTemperature() + 32;
Serial.print("Temperature (oC): ");
Serial.println(temperatureF);*/
// Write to ThingSpeak. There are up to 8 fields in a channel, allowing you to store up to 8 different
// pieces of information in a channel. Here, we write to field 1.
int x = ThingSpeak.writeField(myChannelNumber, 1, temperatureC, myWriteAPIKey);
//uncomment if you want to get temperature in Fahrenheit
//int x = ThingSpeak.writeField(myChannelNumber, 1, temperatureF, myWriteAPIKey);
if(x == 200){
Serial.println("Channel update successful.");
}
else{
Serial.println("Problem updating channel. HTTP error code " + String(x));
}
lastTime = millis();
}
}
View raw code
To make the code work, you need to insert your network credentials in the following variables:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
You need to insert the number of the channel that you're publishing to. If you only have one channel created in ThingSpeak, the channel number is 1. Otherwise, you can see the number of the channel on the Private View tab.
unsigned long myChannelNumber = 1;
Finally, you need to insert the Write API key you've gotten from the previous steps:
const char * myWriteAPIKey = "XXXXXXXXXXXXXXXX";
How the Code Works
First, you need to include the necessary libraries.
#include <WiFi.h>
#include "ThingSpeak.h"
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>
Insert your network credentials in the following variables:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Create a Wi-Fi client to connect to ThingSpeak.
WiFiClient client;
Insert your channel number as well as your write API key:
unsigned long myChannelNumber = 1;
const char * myWriteAPIKey = "XXXXXXXXXXXXXXXX";
In the timerDelay variable insert how frequently you want to publish readings. In this case, we're publishing readings every 30 seconds (30000 milliseconds). You can change this delay time if you want.
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
The temperatureC variable holds the temperature value in Celsius degrees.
float temperatureC;
If you want to get the temperature in Fahrenheit degrees, uncomment the following line.
//float temperatureF;
Create an Adafruit_BME280 object called bme on the default ESP32 pins.
Adafruit_BME280 bme; //BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)
The initBME() function initializes the BME280 sensor.
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
In the setup(), initialize the Serial Monitor:
Serial.begin(115200); //Initialize serial
Initialize the BME280 sensor.
initBME();
Set the ESP32 as a Wi-Fi station:
WiFi.mode(WIFI_STA);
Initialize ThingSpeak:
ThingSpeak.begin(client); // Initialize ThingSpeak
In the loop(), connect or reconnect to Wi-Fi in case the connection was lost:
// Connect or reconnect to WiFi
if(WiFi.status() != WL_CONNECTED){
Serial.print("Attempting to connect");
while(WiFi.status() != WL_CONNECTED){
WiFi.begin(ssid, password);
delay(5000);
}
Serial.println("\nConnected.");
}
Get a new temperature reading and print it in the Serial Monitor:
temperatureC = bme.readTemperature();
Uncomment the following lines if you want to get the temperature in Fahrenheit degrees.
/*temperatureF = 1.8 * bme.readTemperature() + 32;
Serial.print("Temperature (oC): ");
Serial.println(temperatureF);*/
Finally, write to ThingSpeak. You can use the writeField() method that accepts as arguments:
the channel number;
the field number (in our case, we just have one field);
the value you want to publish (temperatureC or temperatureF);
your write API key.
This function returns the code 200 if it has succeeded in publishing the readings.
int x = ThingSpeak.writeField(myChannelNumber, 1, temperatureC, myWriteAPIKey);
if(x == 200){
Serial.println("Channel update successful.");
}
else{
Serial.println("Problem updating channel. HTTP error code " + String(x));
}
If you want to publish your readings in Fahrenheit degrees, uncomment the following line in the code:
//int x = ThingSpeak.writeField(myChannelNumber, 1, temperatureF, myWriteAPIKey);
Demonstration
After inserting your network credentials, channel number and API key, upload the code to your board.
Open the Serial Monitor at a baud rate of 115200, and press the on-board RST button. After 30 seconds, it should connect to Wi-Fi and start publishing the readings to ThingSpeak.
Go to your ThingSpeak account to the channel you've just created, and you'll see the temperature readings being published and plotted on the chart.
Now, you can get access to those readings from anywhere in the world by accessing your ThingSpeak account.
Sending Multiple Fields (Temperature, Humidity, and Pressure)
In this section, you'll learn how to send multiple fieldsthis is sending more than one value at a timewe'll send temperature, humidity, and pressure readings.
Enable Multiple Fields ThingSpeak
First, you need to create more fields in your ThingSpeak account. This is simple. You need to go to your Channel Settings and add as many fields as you want. In this case, we've added two more fields, one for the humidity and another for the pressure.
Finally, save the channelclick the Save Channel button.
Now, if you go to the Private View tab, you should have three charts. Edit the newly created charts with a title and axis labels.
ESP32 Write Multiple Fields to ThingSpeak Code
The following code sends multiple fields to ThingSpeak (temperature, humidity, and pressure from the BME280 sensor).
/*
Adapted from Example from ThingSpeak Library (Mathworks)
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-thingspeak-publish-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
#include "ThingSpeak.h"
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>
const char* ssid = "REPLACE_WITH_YOUR_SSID"; // your network SSID (name)
const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // your network password
WiFiClient client;
unsigned long myChannelNumber = X;
const char * myWriteAPIKey = "XXXXXXXXXXXXXXXX";
// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
// Variable to hold temperature readings
float temperatureC;
float humidity;
float pressure;
//uncomment if you want to get temperature in Fahrenheit
//float temperatureF;
// Create a sensor object
Adafruit_BME280 bme; //BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
void setup() {
Serial.begin(115200); //Initialize serial
initBME();
WiFi.mode(WIFI_STA);
ThingSpeak.begin(client); // Initialize ThingSpeak
}
void loop() {
if ((millis() - lastTime) > timerDelay) {
// Connect or reconnect to WiFi
if(WiFi.status() != WL_CONNECTED){
Serial.print("Attempting to connect");
while(WiFi.status() != WL_CONNECTED){
WiFi.begin(ssid, password);
delay(5000);
}
Serial.println("\nConnected.");
}
// Get a new temperature reading
temperatureC = bme.readTemperature();
Serial.print("Temperature (oC): ");
Serial.println(temperatureC);
humidity = bme.readHumidity();
Serial.print("Humidity (%): ");
Serial.println(humidity);
pressure = bme.readPressure() / 100.0F;
Serial.print("Pressure (hPa): ");
Serial.println(pressure);
//uncomment if you want to get temperature in Fahrenheit
/*temperatureF = 1.8 * bme.readTemperature() + 32;
Serial.print("Temperature (oC): ");
Serial.println(temperatureF);*/
// set the fields with the values
ThingSpeak.setField(1, temperatureC);
//ThingSpeak.setField(1, temperatureF);
ThingSpeak.setField(2, humidity);
ThingSpeak.setField(3, pressure);
// Write to ThingSpeak. There are up to 8 fields in a channel, allowing you to store up to 8 different
// pieces of information in a channel. Here, we write to field 1.
int x = ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);
if(x == 200){
Serial.println("Channel update successful.");
}
else{
Serial.println("Problem updating channel. HTTP error code " + String(x));
}
lastTime = millis();
}
}
View raw code
This code is very similar to the previous one, but sends multiple fields. Let's take a look at the relevant parts for this example.
Frist, create variables to hold the sensor readings:
float temperatureC;
float humidity;
float pressure;
In the loop(), get new temperature, humidity and pressure readings:
// Get a new temperature reading
temperatureC = bme.readTemperature();
Serial.print("Temperature (oC): ");
Serial.println(temperatureC);
humidity = bme.readHumidity();
Serial.print("Humidity (%): ");
Serial.println(humidity);
pressure = bme.readPressure() / 100.0F;
Serial.print("Pressure (hPa): ");
Serial.println(pressure);
You need to assign a value to each field. If you've created the fields as we did, the first field corresponds to the temperature, the second to the humidity, and the third to the pressure. The following lines assign the corresponding values to each field using the setField() methodit accepts as arguments the field number and the value:
// set the fields with the values
ThingSpeak.setField(1, temperatureC);
//ThingSpeak.setField(1, temperatureC);
ThingSpeak.setField(2, humidity);
ThingSpeak.setField(3, pressure);
Finally, use the writeFields() method and set as arguments the channel number and the write API key:
int x = ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);
Demonstration
Upload the previous code to your boarddon't forget to insert your network credentials, Write API Key and channel number. After uploading, it should connect successfully and send the readigns:
If you go to your ThingSpeak account, under Private View, you can see three charts with the sensor readings.
Wrapping Up
In this tutorial you've learned how to publish readings from a BME280 sensor to ThingSpeak using the ESP32 and the thingspeak-arduino library. You can change the examples to send readings from any other sensors or data from any other source. We have tutorials for the most popular sensors:
ESP32 with DHT11/DHT22 Temperature and Humidity Sensor using Arduino IDE
ESP32: BME680 Environmental Sensor using Arduino IDE (Gas, Pressure, Humidity, Temperature)
ESP32 DS18B20 Temperature Sensor with Arduino IDE (Single, Multiple, Web Server)
ESP32 with BMP180 Barometric Sensor Guide
You can visualize the sensor readings from anywhere by logging in to your ThingSpeak account.
The library used throughout this tutorial provides several examples that may be usefulcheck the examples here or in your Arduino IDE go to File > Examples > ThingSpeak, and you'll have several examples.
We hope you've found this tutorial useful. Learn more about the ESP32 with our resources:
Web Server (WebSocket) with Multiple Sliders: Control LEDs Brightness (PWM)
This tutorial shows how to build an ESP32 web server that displays a web page with multiple sliders. The sliders control the duty cycle of different PWM channels to control the brightness of multiple LEDs. Instead of LEDs, you can use this project to control DC motors or other actuators that require a PWM signal. The communication between the clients and the ESP32 is done using WebSocket protocol. Additionally, whenever there's a change, all clients update their slider values simultaneously.
You can also modify the code presented in this tutorial to add sliders to your projects to set threshold values or any other values you need to use in your code.
For this project, the ESP32 board will be programmed using the Arduino core. You can either use the Arduino IDE, VS Code with PlatformIO, or any other suitable IDE.
To better understand how this project works, we recommend taking a look at the following tutorials:
ESP32 PWM with Arduino IDE (Analog Output)
ESP32 WebSocket Server: Control Outputs (Arduino IDE)
ESP32 Web Server with Slider: Control LED Brightness (PWM)*
* This project shows how to build a web server with one slider, but it uses HTTP requestsin this tutorial, we'll use WebSocket protocol.
We have a similar tutorial for the ESP8266 NodeMCU board:
ESP8266 NodeMCU Web Server (WebSocket) with Multiple Sliders: Control LEDs Brightness (PWM)
Project Overview
The following image shows the web page we'll build for this project:
The web page contains three cards;
Each card has a paragraph to display the card title (Fader 1, Fader 2, Fader 3);
There's a range slider in each card that you can move to set the brightness of the corresponding LED;
In each card, another paragraph displays the current LED brightness (in percentage);
When you set a new position for the slider, it updates all clients (if you have multiple web browser tabs opened (or multiple devices), they update almost simultaneously whenever there's a change).
How it Works?
The ESP hosts a web server that displays a web page with three sliders;
When you set a new position for a slider, the client sends the slider number and slider value to the server via WebSocket protocol. For example, if you set slider number 3 to position number 40, it would send this message 3s40 to the server.
The server (ESP) receives the slider number and corresponding value and adjusts the PWM duty cycle accordingly. Additionally, it also notifies all the other clients with the new current slider valuesthis allows us to have all clients updated almost instantaneously.
The ESP32 outputs the PWM signal with the corresponding duty cycle to control the LED brightness. A duty cycle of 0% means the LED is completely off, a duty cycle of 50% means the LED is half lit, and a duty cycle of 100% means the LED is lit;
Whenever you open a new web browser window (this is when a new client connects), it will send a message to the ESP32 (also through WebSocket protocol) with the message getValues. When the ESP32 gets this message, it sends the current slider values. This way, whenever you open a new tab, it always shows the current and updated values.
Prerequisites
Before proceeding with this tutorial, make sure you check all the following prerequisites.
1) Parts Required
To follow this project you need:
ESP32 Board read ESP32 Development Boards Review and Comparison
3x LEDs
3x 220Ohm resistors
Breadboard
Jumper wires
You don't need three LEDs to test this project, you can simply see the results in the Serial Monitor or use other actuators that required a PWM signal to operate.
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
2) Arduino IDE and ESP32 Boards Add-on
We'll program the ESP32 using Arduino IDE. So, you must have the ESP32 add-on installed. Follow the next tutorial if you haven't already:
Installing ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP32:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
3) Filesystem Uploader Plugin
To upload the HTML, CSS, and JavaScript files needed to build this project to the ESP32 flash memory (SPIFFS), we'll use a plugin for Arduino IDE: SPIFFS Filesystem uploader. Follow the next tutorial to install the filesystem uploader plugin if you haven't already:
ESP32: Install SPIFFS FileSystem Uploader Plugin in Arduino IDE
If you're using VS Code with the PlatformIO extension, read the following tutorial to learn how to upload files to the filesystem:
ESP32 with VS Code and PlatformIO: Upload Files to Filesystem (SPIFFS)
4) Libraries
To build this project, you need to install the following libraries:
Arduino_JSON library by Arduino version 0.1.0 (Arduino Library Manager)
ESPAsyncWebServer (.zip folder);
AsyncTCP (.zip folder).
You can install the first library using the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the library name.
The ESPAsyncWebServer and AsynTCP libraries aren't available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch> Include Library > Add .zip Library and select the libraries you've just downloaded.
Installing Libraries (VS Code + PlatformIO)
If you're programming the ESP32 using PlatformIO, you should add the following lines to the platformio.ini file to include the libraries (also change the Serial Monitor speed to 115200):
monitor_speed = 115200
lib_deps = ESP Async WebServer
arduino-libraries/Arduino_JSON @ 0.1.0
Schematic Diagram
Wire three LEDs to the ESP32. We're using GPIOs 12, 13, and 14. You can use any other suitable GPIOs.
Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
Organizing Your Files
To keep the project organized and make it easier to understand, we'll create four files to build the web server:
Arduino sketch that handles the web server;
index.html: to define the content of the web page;
sytle.css: to style the web page;
script.js: to program the behavior of the web pagehandle what happens when you move the slider, send, receive and interpret the messages received via WebSocket protocol.
You should save the HTML, CSS, and JavaScript files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We'll upload these files to the ESP32 filesystem (SPIFFS).
You can download all project files:
Download All the Arduino Project Files
HTML File
Copy the following to the index.html file.
<!-- Complete project details: https://randomnerdtutorials.com/esp32-web-server-websocket-sliders/ -->
<!DOCTYPE html>
<html>
<head>
<title>ESP IOT DASHBOARD</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div>
<h1>Multiple Sliders</h2>
</div>
<div>
<div>
<div>
<p>Fader 1</p>
<p>
<input type="range" onchange="updateSliderPWM(this)" min="0" max="100" step="1" value ="0">
</p>
<p>Brightness: <span></span> %</p>
</div>
<div>
<p> Fader 2</p>
<p>
<input type="range" onchange="updateSliderPWM(this)" min="0" max="100" step="1" value ="0">
</p>
<p>Brightness: <span></span> %</p>
</div>
<div>
<p> Fader 3</p>
<p>
<input type="range" onchange="updateSliderPWM(this)" min="0" max="100" step="1" value ="0">
</p>
<p>Brightness: <span></span> %</p>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
View raw code
Let's take a quick look at the most relevant parts of the HTML file.
Creating a Slider
The following tags create the card for the first slider (Fader 1).
<div>
<p>Fader 1</p>
<p>
<input type="range" onchange="updateSliderPWM(this)" min="0" max="100" step="1" value ="0">
</p>
<p>Brightness: <span></span> %</p>
</div>
The first paragraph displays a title for the card (Fader 1). You can change the text to whatever you want.
<p>Fader 1</p>
To create a slider in HTML you use the <input> tag. The <input> tag specifies a field where the user can enter data.
There are a wide variety of input types. To define a slider, use the type attribute with the range value. In a slider, you also need to define the minimum and the maximum range using the min and max attributes (in this case, 0 and 100, respectively).
You also need to define other attributes like:
the step attribute specifies the interval between valid numbers. In our case, we set it to 1;
the class to style the slider (class=slider);
the id so that we can manipulate the slider value using JavaScript (id=slider1);
the onchange attribute to call a function (updateSliderPWM(this)) when you set a new position for the slider. This function (defined in the JavaScript file) sends the current slider value via the WebSocket protocol to the client. The this keyword refers to the HTML slider element.
The slider is inside a paragraph with the switch class name. So, here are the tags that actually create the slider.
<p>
<input type="range" onchange="updateSliderPWM(this)" min="0" max="100" step="1" value ="0">
</p>
Finally, there's a paragraph with a <span> tag, so that we can insert the current slider value in that paragraph by referring to its id (id=sliderValue1).
<p>Brightness: <span></span> %</p>
Creating More Sliders
To create more sliders, you need to copy all the HTML tags that create the complete card. First, however, you need to consider that you need a unique id for each slider and slider value. In our case, we have three sliders with the following ids: slider1, slider2, slider3, and three placeholders for the slider value with the following ids: sliderValue1, sliderValue2, sliderValue3.
For example, here's the card for slider number 2.
<div>
<p> Fader 2</p>
<p>
<input type="range" onchange="updateSliderPWM(this)" min="0" max="100" step="1" value ="0">
</p>
<p>Brightness: <span></span> %</p>
</div>
CSS File
Copy the following to the style.css file.
/* Complete project details: https://randomnerdtutorials.com/esp32-web-server-websocket-sliders/ */
html {
font-family: Arial, Helvetica, sans-serif;
display: inline-block;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
p {
font-size: 1.4rem;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
}
body {
margin: 0;
}
.content {
padding: 30px;
}
.card-grid {
max-width: 700px;
margin: 0 auto;
display: grid;
grid-gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.card {
background-color: white;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title {
font-size: 1.2rem;
font-weight: bold;
color: #034078
}
.state {
font-size: 1.2rem;
color:#1282A2;
}
.slider {
-webkit-appearance: none;
margin: 0 auto;
width: 100%;
height: 15px;
border-radius: 10px;
background: #FFD65C;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 30px;
height: 30px;
border-radius: 50%;
background: #034078;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 30px;
height: 30px;
border-radius: 50% ;
background: #034078;
cursor: pointer;
}
.switch {
padding-left: 5%;
padding-right: 5%;
}
View raw code
Let's take a quick look at the relevant parts of the CSS file that style the slider. In this example, we need to use the vendor prefixes for the appearance attribute.
.slider {
-webkit-appearance: none;
margin: 0 auto;
width: 100%;
height: 15px;
border-radius: 10px;
background: #FFD65C;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 30px;
height: 30px;
border-radius: 50%;
background: #034078;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 30px;
height: 30px;
border-radius: 50% ;
background: #034078;
cursor: pointer;
}
.switch {
padding-left: 5%;
padding-right: 5%;
}
Vendor Prefixes
Vendor prefixes allow a browser to support new CSS features before they become fully supported. The most commonly used browsers use the following prefixes:
-webkit- Chrome, Safari, newer versions of Opera, almost all iOS browsers,
-moz- Firefox,
-o- Old versions of Opera,
-ms- Microsoft Edge and Internet Explorer.
Vendor prefixes are temporary. Once the properties are fully supported by the browser you use, you don't need them. You can use the following reference to check if the property you're using needs prefixes: http://shouldiprefix.com/
Let's take a look at the .slider selector (styles the slider itself):
.slider {
-webkit-appearance: none;
margin: 0 auto;
width: 100%;
height: 15px;
border-radius: 10px;
background: #FFD65C;outline: none;
}
Setting -webkit-appearance to none overrides the default CSS styles applied to the slider in Google Chrome, Safari, and Android browsers.
-webkit-appearance: none;
Setting the margin to 0 auto aligns the slider inside its parent container.
margin: 0 auto;
The width of the slider is set to 100% and the height to 15px. The border-radius is set to 10px.
margin: 0 auto;
width: 100%;
height: 15px;
border-radius: 10px;
Set the background color for the slider and set the outline to none.
background: #FFD65C;
outline: none;
Then, format the slider handle. Use -webkit- for Chrome, Opera, Safari and Edge web browsers and -moz- for Firefox.
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 30px;
height: 30px;
border-radius: 50%;
background: #034078;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 30px;
height: 30px;
border-radius: 50% ;
background: #034078;
cursor: pointer;
}
Set the -webkit-appearance and appearance properties to none to override default properties.
-webkit-appearance: none;
appearance: none;
Set a specific width, height and border-radius for the handler. Setting the same width and height with a border-radius of 50% creates a circle.
width: 30px;
height: 30px;
border-radius: 50%;
Then, set a color for the background and set the cursor to a pointer.
background: #034078;
cursor: pointer;
Feel free to play with the slider properties to give it a different look.
JavaScript File
Copy the following to the script.js file.
// Complete project details: https://randomnerdtutorials.com/esp32-web-server-websocket-sliders/
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onload);
function onload(event) {
initWebSocket();
}
function getValues(){
websocket.send("getValues");
}
function initWebSocket() {
console.log('Trying to open a WebSocket connection');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
function onOpen(event) {
console.log('Connection opened');
getValues();
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function updateSliderPWM(element) {
var sliderNumber = element.id.charAt(element.id.length-1);
var sliderValue = document.getElementById(element.id).value;
document.getElementById("sliderValue"+sliderNumber).innerHTML = sliderValue;
console.log(sliderValue);
websocket.send(sliderNumber+"s"+sliderValue.toString());
}
function onMessage(event) {
console.log(event.data);
var myObj = JSON.parse(event.data);
var keys = Object.keys(myObj);
for (var i = 0; i < keys.length; i++){
var key = keys[i];
document.getElementById(key).innerHTML = myObj[key];
document.getElementById("slider"+ (i+1).toString()).value = myObj[key];
}
}
View raw code
Here's a list of what this code does:
initializes a WebSocket connection with the server;
sends a message to the server to get the current slider values;
uses the response to update the slider values on the web page;
handles data exchange through the WebSocket protocol.
Let's take a look at this JavaScript code to see how it works.
The gateway is the entry point to the WebSocket interface. window.location.hostname gets the current page address (the web server IP address).
var gateway = ws://${window.location.hostname}/ws;
Create a new global variable called websocket.
var websocket;
Add an event listener that will call the onload function when the web page loads.
window.addEventListener('load', onload);
The onload() function calls the initWebSocket() function to initialize a WebSocket connection with the server.
function onload(event) {
initWebSocket();
}
The initWebSocket() function initializes a WebSocket connection on the gateway defined earlier. We also assign several callback functions for when the WebSocket connection is opened, closed, or when a message is received.
function initWebSocket() {
console.log('Trying to open a WebSocket connection');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
Note that when the websocket connection in open, we'll call the getValues function.
function onOpen(event) {
console.log('Connection opened');
getValues();
}
The getValues() function sends a message to the server getValues to get the current value of all sliders. Then, we must handle what happens when we receive that message on the server side (ESP32).
function getStates(){
websocket.send("getValues");
}
We handle the messages received via websocket protocol on the onMessage() function.
function onMessage(event) {
console.log(event.data);
var myObj = JSON.parse(event.data);
var keys = Object.keys(myObj);
for (var i = 0; i < keys.length; i++){
var key = keys[i];
document.getElementById(key).innerHTML = myObj[key];
document.getElementById("slider"+ (i+1).toString()).value = myObj[key];
}
}
The server sends the states in JSON format, for example:
{
sliderValue1 : 20;
sliderValue2: 50;
sliderValue3: 0;
}
The onMessage() function simply goes through all the values and places them on the corresponding places on the HTML page.
The updateSliderPWM() function runs when you move the sliders.
function updateSliderPWM(element) {
var sliderNumber = element.id.charAt(element.id.length-1);
var sliderValue = document.getElementById(element.id).value;
document.getElementById("sliderValue"+sliderNumber).innerHTML = sliderValue;
console.log(sliderValue);
websocket.send(sliderNumber+"s"+sliderValue.toString());
}
This function gets the value from the slider and updates the corresponding paragraph with the right value. This function also sends a message to the server so that the ESP32 updates the LED brightness.
websocket.send(sliderNumber+"s"+sliderValue.toString());
The message is sent in the following format:
slidernumbersslidervalue
For example, if you move slider number 3 to position 40, it will send the following message:
3s40
Arduino Sketch
Copy the following code to your Arduino IDE or to the main.cpp file if you're using PlatformIO.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-web-server-websocket-sliders/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
// Create a WebSocket object
AsyncWebSocket ws("/ws");
// Set LED GPIO
const int ledPin1 = 12;
const int ledPin2 = 13;
const int ledPin3 = 14;
String message = "";
String sliderValue1 = "0";
String sliderValue2 = "0";
String sliderValue3 = "0";
int dutyCycle1;
int dutyCycle2;
int dutyCycle3;
// setting PWM properties
const int freq = 5000;
const int ledChannel1 = 0;
const int ledChannel2 = 1;
const int ledChannel3 = 2;
const int resolution = 8;
//Json Variable to Hold Slider Values
JSONVar sliderValues;
//Get Slider Values
String getSliderValues(){
sliderValues["sliderValue1"] = String(sliderValue1);
sliderValues["sliderValue2"] = String(sliderValue2);
sliderValues["sliderValue3"] = String(sliderValue3);
String jsonString = JSON.stringify(sliderValues);
return jsonString;
}
// Initialize SPIFFS
void initFS() {
if (!SPIFFS.begin()) {
Serial.println("An error has occurred while mounting SPIFFS");
}
else{
Serial.println("SPIFFS mounted successfully");
}
}
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void notifyClients(String sliderValues) {
ws.textAll(sliderValues);
}
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
message = (char*)data;
if (message.indexOf("1s") >= 0) {
sliderValue1 = message.substring(2);
dutyCycle1 = map(sliderValue1.toInt(), 0, 100, 0, 255);
Serial.println(dutyCycle1);
Serial.print(getSliderValues());
notifyClients(getSliderValues());
}
if (message.indexOf("2s") >= 0) {
sliderValue2 = message.substring(2);
dutyCycle2 = map(sliderValue2.toInt(), 0, 100, 0, 255);
Serial.println(dutyCycle2);
Serial.print(getSliderValues());
notifyClients(getSliderValues());
}
if (message.indexOf("3s") >= 0) {
sliderValue3 = message.substring(2);
dutyCycle3 = map(sliderValue3.toInt(), 0, 100, 0, 255);
Serial.println(dutyCycle3);
Serial.print(getSliderValues());
notifyClients(getSliderValues());
}
if (strcmp((char*)data, "getValues") == 0) {
notifyClients(getSliderValues());
}
}
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
void setup() {
Serial.begin(115200);
pinMode(ledPin1, OUTPUT);
pinMode(ledPin2, OUTPUT);
pinMode(ledPin3, OUTPUT);
initFS();
initWiFi();
// configure LED PWM functionalitites
ledcSetup(ledChannel1, freq, resolution);
ledcSetup(ledChannel2, freq, resolution);
ledcSetup(ledChannel3, freq, resolution);
// attach the channel to the GPIO to be controlled
ledcAttachPin(ledPin1, ledChannel1);
ledcAttachPin(ledPin2, ledChannel2);
ledcAttachPin(ledPin3, ledChannel3);
initWebSocket();
// Web Server Root URL
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
server.serveStatic("/", SPIFFS, "/");
// Start server
server.begin();
}
void loop() {
ledcWrite(ledChannel1, dutyCycle1);
ledcWrite(ledChannel2, dutyCycle2);
ledcWrite(ledChannel3, dutyCycle3);
ws.cleanupClients();
}
View raw code
How the Code Works
Let's take a quick look at the relevant parts for this project. To better understand how the code works, we recommend following this tutorial about WebSocket protocol with the ESP32 and this tutorial about PWM with the ESP32.
Insert your network credentials in the following variables to connect the ESP32 to your local network:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
The getSliderValues() function creates a JSON string with the current slider values.
String getSliderValues(){
sliderValues["sliderValue1"] = String(sliderValue1);
sliderValues["sliderValue2"] = String(sliderValue2);
sliderValues["sliderValue3"] = String(sliderValue3);
String jsonString = JSON.stringify(sliderValues);
return jsonString;
}
The notifyClients() function notifies all clients with the current slider values. Calling this function is what allows us to notify changes in all clients whenever you set a new position for a slider.
void notifyClients(String sliderValues) {
ws.textAll(sliderValues);
}
The handleWebSocketMessage(), as the name suggests, handles what happens when the server receives a message from the client via WebSocket protocol. We've seen in the JavaScript file, that the server can receive the getValues message or a message with the slider number and the slider value.
When it receives the getValues message, it sends the current slider values.
if (strcmp((char*)data, "getValues") == 0) {
notifyClients(getSliderValues());
}
If it receives another message, we check to which slider corresponds the message and update the corresponding duty cycle value. Finally, we notify all clients that a change occurred. Here's an example for slider 1:
if (message.indexOf("1s") >= 0) {
sliderValue1 = message.substring(2);
dutyCycle1 = map(sliderValue1.toInt(), 0, 100, 0, 255);
Serial.println(dutyCycle1);
Serial.print(getSliderValues());
notifyClients(getSliderValues());
}
In the loop(), we update the duty cycle of the PWM channels to adjust the brightness of the LEDs.
void loop() {
ledcWrite(ledChannel1, dutyCycle1);
ledcWrite(ledChannel2, dutyCycle2);
ledcWrite(ledChannel3, dutyCycle3);
ws.cleanupClients();
}
Upload Code and Files
After inserting your network credentials, save the code. Go to Sketch > Show Sketch Folder, and create a folder called data.
Inside that folder you should save the HTML, CSS and JavaScript files.
Then, upload the code to your ESP32 board. Make sure you have the right board and COM port selected. Also, make sure you've added your network credentials.
After uploading the code, you need to upload the files. Go to Tools > ESP32 Data Sketch Upload and wait for the files to be uploaded.
When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the ESP32 EN/RST button, and it should print the ESP32 IP address.
Demonstration
Open a browser on your local network and paste the ESP32 IP address. You should get access to the web server page to control the brightness of the LEDs.
Move the sliders to control the brightness of the LEDs.
Open several tabs or connect to the web server using another device, and notice that the slider values update almost instantaneously whenever there's a change.
You can watch the video demonstration:
Wrapping Up
In this tutorial, you've learned how to build a web server with the ESP32 that serves a web page with multiple sliders. The sliders allow you to control the brightness of LEDs connected to the ESP32. In addition, we've used the WebSocket protocol to communicate between the ESP32 and the clients.
We hope you had learned a lot from this tutorial. Let us know in the comments below if you successfully followed this tutorial and got the project working.
Installing ESP32 Board in Arduino IDE 2.0 (Windows, Mac OS X, Linux)
There is a new Arduino IDEArduino IDE 2.0 (beta version). In this tutorial, you'll learn how to install the ESP32 boards in Arduino IDE 2.0 and upload code to the board. This tutorial is compatible with Windows, Mac OS X, and Linux operating systems.
Accordingly to the Arduino website: The Arduino IDE 2.0 is an improvement of the classic IDE, with increased performance, improved user interface and many new features, such as autocompletion, a built-in debugger and syncing sketches with Arduino Cloud.
If you want to install the ESP32 boards on the classic Arduino IDE, follow the next tutorial instead:
Installing the ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
If you prefer programming the ESP32 using VS Code + PlatformIO, go to the following tutorial:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
You might also like reading the ESP8266 Guide: Installing ESP8266 NodeMCU Board in Arduino 2.0 (Windows, Mac OS X, Linux)
Prerequisites: Arduino IDE 2.0 Installed
Before proceeding make sure you have Arduino IDE 2.0 installed on your computer.
Go to the Arduino website and download the version for your operating system.
Windows: run the file downloaded and follow the instructions in the installation guide.
Mac OS X: copy the downloaded file into your application folder.
Linux: extract the downloaded file, and open the arduino-ide file that will launch the IDE.
If you have doubts, you can go to the Arduino Installation Guide.
Do you need an ESP32 board? You can buy it here.
Recommended reading: ESP32 Development Boards Review and Comparison
Install ESP32 Add-on in Arduino IDE
To install the ESP32 board in your Arduino IDE, follow these next instructions:
1. In your Arduino IDE 2.0, go to File > Preferences.
2. Copy and paste the following line to the Additional Boards Manager URLs field.
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
Note: if you already have the ESP8266 boards URL, you can separate the URLs with a comma, as follows:
http://arduino.esp8266.com/stable/package_esp8266com_index.json, https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
3. Open the Boards Manager. You can go to Tools > Board > Boards Manager or you can simply click the Boards Manager icon in the left-side corner.
4. Search for ESP32 and press the install button for esp32 by Espressif Systems.
That's it. It should be installed after a few seconds.
Testing the Installation
To test the ESP32 add-on installation, we'll upload a simple code that blinks the on-board LED (GPIO 2).
Copy the following code to your Arduino IDE:
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/
*********/
#include <Arduino.h>
#define LED 2
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
pinMode(LED, OUTPUT);
}
void loop() {
// put your main code here, to run repeatedly:
digitalWrite(LED, HIGH);
Serial.println("LED is on");
delay(1000);
digitalWrite(LED, LOW);
Serial.println("LED is off");
delay(1000);
}
View raw code
Uploading the Sketch
On the top drop-down menu, select the unknown board. A new window, as shown below, will open.
You should select your ESP32 board model and the COM port. In our example, we're using the DOIT ESP32 DEVKIT V1 board. Click OK when you're done.
Now, you just need to click on the Upload button.
After a few seconds, the upload should be complete.
Note: some ESP32 development boards don't go into flashing/uploading mode automatically when uploading a new code and you'll see a lot of dots on the debugging window followed by an error message. If that's the case, you need to press the ESP32 BOOT button when you start seeing the dots on the debugging window.
The ESP32 on-board LED should be blinking every second.
Serial Monitor
You can click on the Serial Monitor icon to open the Serial Monitor tab.
That's it! You've installed the ESP32 Boards successfully in Arduino IDE 2.0.
Wrapping Up
This is a quick guide that shows how to prepare Arduino IDE 2.0 for the ESP32 Boards on a Windows PC, Mac OS X, or Linux computer.
Plot Sensor Readings in Charts (Multiple Series)
This project shows how to build a web server with the ESP32 to plot sensor readings in charts with multiple series. As an example, we'll plot sensor readings from four different DS18B20 temperature sensors on the same chart. You can modify the project to plot any other data. To build the charts, we'll use the Highcharts JavaScript library.
We have a similar tutorial for the ESP8266 NodeMCU board:
ESP8266 NodeMCU Plot Sensor Readings in Charts (Multiple Series)
Project Overview
This project will build a web server with the ESP32 that displays temperature readings from four DS18B20 temperature sensors on the same chartchart with multiple series. The chart displays a maximum of 40 data points for each series, and new readings are added every 30 seconds. You can change these values in your code.
DS18B20 Temperature Sensor
The DS18B20 temperature sensor is a one-wire digital temperature sensor. This means that it just requires one data line to communicate with your microcontroller.
Each sensor has a unique 64-bit serial number, which means you can connect multiple sensors to the same GPIOas we'll do in this tutorial. Learn more about the DS18B20 temperature sensor:
ESP32 DS18B20 Temperature Sensor with Arduino IDE (Single, Multiple, Web Server)
ESP32 with Multiple DS18B20 Temperature Sensors
Server-Sent Events
The readings are updated automatically on the web page using Server-Sent Events (SSE).
To learn more about SSE, you can read:
ESP32 Web Server using Server-Sent Events (Update Sensor Readings Automatically)
Files Saved on the Filesystem
To keep our project better organized and easier to understand, we'll save the HTML, CSS, and JavaScript files to build the web page on the board's filesystem (SPIFFS).
Learn more about building a web server with files saved on the filesystem:
ESP32 Web Server using SPIFFS (SPI Flash File System)
Prerequisites
Make sure you check all the prerequisites in this section before continuing with the project.
1. Install ESP32 Board in Arduino IDE
We'll program the ESP32 using Arduino IDE. So, you must have the ESP32 add-on installed. Follow the next tutorial if you haven't already:
Installing ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP32:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
2. Filesystem Uploader Plugin
To upload the HTML, CSS, and JavaScript files to the ESP32 flash memory (SPIFFS), we'll use a plugin for Arduino IDE: SPIFFS Filesystem uploader. Follow the next tutorial to install the filesystem uploader plugin:
ESP32: Install SPIFFS FileSystem Uploader Plugin in Arduino IDE
If you're using VS Code with the PlatformIO extension, read the following tutorial to learn how to upload files to the filesystem:
ESP32 with VS Code and PlatformIO: Upload Files to Filesystem (SPIFFS)
3. Installing Libraries
To build this project, you need to install the following libraries:
OneWire (by Paul Stoffregen) (Arduino Library Manager);
DallasTemperature (Arduino Library Manager);
Arduino_JSON library by Arduino version 0.1.0 (Arduino Library Manager)
ESPAsyncWebServer (.zip folder);
AsyncTCP (.zip folder).
You can install the first two libraries using the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the library name.
The ESPAsyncWebServer and AsynTCP libraries aren't available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch > Include Library > Add .zip Library and select the libraries you've just downloaded.
Installing Libraries (VS Code + PlatformIO)
If you're programming the ESP32 using PlatformIO, you should add the following lines on the platformio.ini file to include the libraries (also change the Serial Monitor speed to 115200):
monitor_speed = 115200
lib_deps = ESP Async WebServer
arduino-libraries/Arduino_JSON @ 0.1.0
milesburton/DallasTemperature@^3.9.1
paulstoffregen/OneWire@^2.3.5
Parts Required
To follow this tutorial you need the following parts:
ESP32 (read Best ESP32 development boards)
4x DS18B20 temperature sensor (one or multiple sensors) waterproof version
4.7k Ohm resistor
Jumper wires
Breadboard
If you don't have four DS18B20 sensors, you can use three or two. Alternatively, you can also use other sensors (you need to modify the code) or data from any other source (for example, sensor readings received via MQTT, ESP-NOW, or random valuesto experiment with this project)
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic Diagram
Wire four DS18B20 sensors to your board.
Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
Getting the DS18B20 Sensors' Addresses
Each DS18B20 temperature sensor has an assigned serial number. First, you need to find that number to label each sensor accordingly. You need to do this so that later you know from which sensor you're reading the temperature.
Upload the following code to the ESP32. Make sure you have the right board and COM port selected.
/*
* Rui Santos
* Complete Project Details https://randomnerdtutorials.com
*/
#include <OneWire.h>
// Based on the OneWire library example
OneWire ds(4); //data wire connected to GPIO 4
void setup(void) {
Serial.begin(115200);
}
void loop(void) {
byte i;
byte addr[8];
if (!ds.search(addr)) {
Serial.println(" No more addresses.");
Serial.println();
ds.reset_search();
delay(250);
return;
}
Serial.print(" ROM =");
for (i = 0; i < 8; i++) {
Serial.write(' ');
Serial.print(addr[i], HEX);
}
}
View raw code
Wire just one sensor at a time to find its address (or successively add a new sensor) so that you're able to identify each one by its address. Then, you can add a physical label to each sensor.
Open the Serial Monitor at a baud rate of 115200, press the on-board RST/EN button and you should get something as follows (but with different addresses):
Untick the Autoscroll option so that you're able to copy the addresses. In our case, we've got the following addresses:
Sensor 1: 28 FF A0 11 33 17 3 96
Sensor 2: 28 FF B4 6 33 17 3 4B
Sensor 3: 28 FF 11 28 33 18 1 6B
Sensor 4: 28 FF 43 F5 32 18 2 A8
Organizing Your Files
To keep the project organized and make it easier to understand, we'll create four files to build the web server:
Arduino sketch that handles the web server;
index.html: to define the content of the web page;
sytle.css: to style the web page;
script.js: to program the behavior of the web pagehandle web server responses, events, create the chart, etc.
You should save the HTML, CSS, and JavaScript files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We'll upload these files to the ESP32 filesystem (SPIFFS).
You can download all project files:
Download All the Arduino Project Files
HTML File
Copy the following to the index.html file.
<!-- Complete project details: https://randomnerdtutorials.com/esp32-plot-readings-charts-multiple/ -->
<!DOCTYPE html>
<html>
<head>
<title>ESP IOT DASHBOARD</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://code.highcharts.com/highcharts.js"></script>
</head>
<body>
<div>
<h1>ESP WEB SERVER CHARTS</h2>
</div>
<div>
<div>
<div>
<p>Temperature Chart</p>
<div></div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
View raw code
The HTML file for this project is very simple. It includes the JavaScript Highcharts library in the head of the HTML file:
<script src="https://code.highcharts.com/highcharts.js"></script>
There is a <div> section with the id chart-temperature where we'll render our chart later on.
<div></div>
CSS File
Copy the following styles to your style.css file.
/* Complete project details: https://randomnerdtutorials.com/esp32-plot-readings-charts-multiple/ */
html {
font-family: Arial, Helvetica, sans-serif;
display: inline-block;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
p {
font-size: 1.4rem;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
}
body {
margin: 0;
}
.content {
padding: 5%;
}
.card-grid {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.card {
background-color: white;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title {
font-size: 1.2rem;
font-weight: bold;
color: #034078
}
.chart-container {
padding-right: 5%;
padding-left: 5%;
}
View raw code
JavaScript File (creating the charts)
Copy the following to the script.js file. Here's a list of what this code does:
initializing the event source protocol;
adding an event listener for the new_readings event;
creating the chart;
getting the latest sensor readings from the new_readings event and plot them in the chart;
making an HTTP GET request for the current sensor readings when you access the web page for the first time.
// Complete project details: https://randomnerdtutorials.com/esp32-plot-readings-charts-multiple/
// Get current sensor readings when the page loads
window.addEventListener('load', getReadings);
// Create Temperature Chart
var chartT = new Highcharts.Chart({
chart:{
renderTo:'chart-temperature'
},
series: [
{
name: 'Temperature #1',
type: 'line',
color: '#101D42',
marker: {
symbol: 'circle',
radius: 3,
fillColor: '#101D42',
}
},
{
name: 'Temperature #2',
type: 'line',
color: '#00A6A6',
marker: {
symbol: 'square',
radius: 3,
fillColor: '#00A6A6',
}
},
{
name: 'Temperature #3',
type: 'line',
color: '#8B2635',
marker: {
symbol: 'triangle',
radius: 3,
fillColor: '#8B2635',
}
},
{
name: 'Temperature #4',
type: 'line',
color: '#71B48D',
marker: {
symbol: 'triangle-down',
radius: 3,
fillColor: '#71B48D',
}
},
],
title: {
text: undefined
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: { second: '%H:%M:%S' }
},
yAxis: {
title: {
text: 'Temperature Celsius Degrees'
}
},
credits: {
enabled: false
}
});
//Plot temperature in the temperature chart
function plotTemperature(jsonValue) {
var keys = Object.keys(jsonValue);
console.log(keys);
console.log(keys.length);
for (var i = 0; i < keys.length; i++){
var x = (new Date()).getTime();
console.log(x);
const key = keys[i];
var y = Number(jsonValue[key]);
console.log(y);
if(chartT.series[i].data.length > 40) {
chartT.series[i].addPoint([x, y], true, true, true);
} else {
chartT.series[i].addPoint([x, y], true, false, true);
}
}
}
// Function to get current readings on the webpage when it loads for the first time
function getReadings(){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var myObj = JSON.parse(this.responseText);
console.log(myObj);
plotTemperature(myObj);
}
};
xhr.open("GET", "/readings", true);
xhr.send();
}
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('message', function(e) {
console.log("message", e.data);
}, false);
source.addEventListener('new_readings', function(e) {
console.log("new_readings", e.data);
var myObj = JSON.parse(e.data);
console.log(myObj);
plotTemperature(myObj);
}, false);
}
View raw code
Get Readings
When you access the web page for the first time, we'll request the server to get the current sensor readings. Otherwise, we would have to wait for new sensor readings to arrive (via Server-Sent Events), which can take some time depending on the interval that you set on the server.
Add an event listener that calls the getReadings function when the web page loads.
// Get current sensor readings when the page loads
window.addEventListener('load', getReadings);
The window object represents an open window in a browser. The addEventListener() method sets up a function to be called when a certain event happens. In this case, we'll call the getReadings function when the page loads (load') to get the current sensor readings.
Now, let's take a look at the getReadings function. Create a new XMLHttpRequest object. Then, send a GET request to the server on the /readings URL using the open() and send() methods.
function getReadings() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/readings", true);
xhr.send();
}
When we send that request, the ESP will send a response with the required information. So, we need to handle what happens when we receive the response. We'll use the onreadystatechange property that defines a function to be executed when the readyState property changes. The readyState property holds the status of the XMLHttpRequest. The response of the request is ready when the readyState is 4, and the status is 200.
readyState = 4 means that the request finished and the response is ready;
status = 200 means OK
So, the request should look something like this:
function getStates(){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
DO WHATEVER YOU WANT WITH THE RESPONSE
}
};
xhr.open("GET", "/states", true);
xhr.send();
}
The response sent by the ESP is the following text in JSON format.
{
"sensor1" : "25",
"sensor2" : "21",
"sensor3" : "22",
"sensor4" : "23"
}
We need to convert the JSON string into a JSON object using the parse() method. The result is saved on the myObj variable.
var myObj = JSON.parse(this.responseText);
The myObj varible is a JSON object that contains all the temperature readings. We want to plot those readings on the same chart. For that, we've created a function called plotTemperature() that plots the temperatures stored in a JSON object on a chart.
plotTemperature(myObj);
Here's the complete getReadings() function.
function getReadings(){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var myObj = JSON.parse(this.responseText);
console.log(myObj);
plotTemperature(myObj);
}
};
xhr.open("GET", "/readings", true);
xhr.send();
}
Creating the Chart
The following lines create the charts with multiple series.
// Create Temperature Chart
var chartT = new Highcharts.Chart({
chart:{
renderTo:'chart-temperature'
},
series: [
{
name: 'Temperature #1',
type: 'line',
color: '#101D42',
marker: {
symbol: 'circle',
radius: 3,
fillColor: '#101D42',
}
},
{
name: 'Temperature #2',
type: 'line',
color: '#00A6A6',
marker: {
symbol: 'square',
radius: 3,
fillColor: '#00A6A6',
}
},
{
name: 'Temperature #3',
type: 'line',
color: '#8B2635',
marker: {
symbol: 'triangle',
radius: 3,
fillColor: '#8B2635',
}
},
{
name: 'Temperature #4',
type: 'line',
color: '#71B48D',
marker: {
symbol: 'triangle-down',
radius: 3,
fillColor: '#71B48D',
}
},
],
title: {
text: undefined
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: { second: '%H:%M:%S' }
},
yAxis: {
title: {
text: 'Temperature Celsius Degrees'
}
},
credits: {
enabled: false
}
});
To create a new chart, use the new Highcharts.Chart() method and pass as argument the chart properties.
var chartT = new Highcharts.Chart({
In the next line, define where you want to put the chart. In our example, we want to place it in the HTML element with the chart-temperature idsee the HTML file section.
chart:{
renderTo:'chart-temperature'
},
Then, define the options for the series. The following lines create the first series:
series: [
{
name: 'Temperature #1',
type: 'line',
color: '#101D42',
marker: {
symbol: 'circle',
radius: 3,
fillColor: '#101D42',
}
The name property defines the series name. The type property defines the type of chartin this case, we want to build a line chart. The color refers to the color of the lineyou can change it to whatever color you desire.
Next, define the marker properties. You can choose from several default symbolssquare, circle, diamond, triangle, triangle-down. You can also create your own symbols. The radius refers to the size of the marker, and the fillColor refers to the color of the marker. There are other properties you can use to customize the markerlearn more.
marker: {
symbol: 'circle',
radius: 3,
fillColor: '#101D42',
}
Creating the other series is similar, but we've chosen different names, markers and colors.
There are many other options you can use to customize your seriescheck the documentation about plotOptions.
You can also define the chart titlein this case, as we've already defined a title for the chart in a heading of the HTML file, we will not set the title here. The title is displayed by default, so we must set it to undefined.
title: {
text: undefined
},
Define the properties for the X axisthis is the axis where we'll display data and time. Check more options to customize the X axis.
xAxis: {
type: 'datetime',
dateTimeLabelFormats: { second: '%H:%M:%S' }
},
We set the title for the y axis. See all available properties for the y axis.
yAxis: {
title: {
text: 'Temperature Celsius Degrees'
}
}
Time Zone
If, for some reason, after building the project, the charts are not showing the right time zone, add the following lines to the JavaScript file after the second line:
Highcharts.setOptions({
time: {
timezoneOffset: -60 //Add your time zone offset here in minutes
}
});
The charts will show the time in UTC. If you want it to display in your timezone, you must set the useUTC parameter (which is a time parameter) as false:
time:{
useUTC: false
},
So, add that when creating the chart as follows:
var chart = new Highcharts.Chart({
time:{
useUTC: false
},
()
To learn more about this property, check this link on the documentation: https://api.highcharts.com/highcharts/time.useUTC
Finally, set the credits option to false to hide the credits of the Highcharts library.
credits: {
enabled: false
}
Plot Temperatures
We've created the plotTemperature() function that accepts as an argument a JSON object with the temperature readings we want to plot.
//Plot temperature in the temperature chart
function plotTemperature(jsonValue) {
var keys = Object.keys(jsonValue);
console.log(keys);
console.log(keys.length);
for (var i = 0; i < keys.length; i++){
var x = (new Date()).getTime();
console.log(x);
const key = keys[i];
var y = Number(jsonValue[key]);
console.log(y);
if(chartT.series[i].data.length > 40) {
chartT.series[i].addPoint([x, y], true, true, true);
} else {
chartT.series[i].addPoint([x, y], true, false, true);
}
}
}
First, we get the keys of our JSON object and save them on the keys variable. This allows us to go through all the keys in the object.
var keys = Object.keys(jsonValue);
The keys variable will be an array with all the keys in the JSON object. In our case:
["sensor1", "sensor2", "sensor3", "sensor4"]
This works if you have a JSON object with a different number of keys or with different keys. Then, we'll go through all the keys (keys.length()) to plot each of its value in the chart.
The x value for the chart is the timestamp.
var x = (new Date()).getTime()
The key variable holds the current key in the loop. The first time we go through the loop, the key variable is sensor1.
const key = keys[i];
Then, we get the value of the key (jsonValue[key]) and save it as a number in the y variable.
Our chart has multiple series (index starts at 0). We can access the first series in the
temperature chart using: chartT.series[0], which corresponds to chartT.series[i] the first time we go through the loop.
First, we check the series data length:
If the series has more than 40 points: append and shift a new point;
Or if the series has less than 40 points: append a new point.
To add a new point use the addPoint() method that accepts the following arguments:
The value to be plotted. If it is a single number, a point with that y value is
appended to the series. If it is an array, it will be interpreted as x and y values. In our case, we pass an array with the x and y values;
Redraw option (boolean): set to true to redraw the chart after the point is added.
Shift option (boolean): If true, a point is shifted off the start of the series as one is appended to the end. When the chart length is bigger than 40, we set the shift option to true.
withEvent option (boolean): Used internally to fire the series addPoint eventlearn more here.
So, to add a point to the chart, we use the next lines:
if(chartT.series[i].data.length > 40) {
chartT.series[i].addPoint([x, y], true, true, true);
} else {
chartT.series[i].addPoint([x, y], true, false, true);
}
Handle events
Plot the readings on the charts when the client receives the readings on the new_readings event.
Create a new EventSource object and specify the URL of the page sending the updates. In our case, it's /events.
if (!!window.EventSource) {
var source = new EventSource('/events');
Once you've instantiated an event source, you can start listening for messages from the server with addEventListener().
These are the default event listeners, as shown here in the AsyncWebServer documentation.
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('message', function(e) {
console.log("message", e.data);
}, false);
Then, add the event listener for new_readings.
source.addEventListener('new_readings', function(e) {
When new readings are available, the ESP32 sends an event (new_readings) to the client. The following lines handle what happens when the browser receives that event.
source.addEventListener('new_readings', function(e) {
console.log("new_readings", e.data);
var myObj = JSON.parse(e.data);
console.log(myObj);
plotTemperature(myObj);
}, false);
Basically, print the new readings on the browser console, convert the data into a JSON object and plot the readings on the chart by calling the plotTemperature() function.
Arduino Sketch
Copy the following code to your Arduino IDE or to the main.cpp file if you're using PlatformIO.
/*********
Rui Santos
Complete instructions at https://RandomNerdTutorials.com/esp32-plot-readings-charts-multiple/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>
#include <OneWire.h>
#include <DallasTemperature.h>
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
// Create an Event Source on /events
AsyncEventSource events("/events");
// Json Variable to Hold Sensor Readings
JSONVar readings;
// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
// GPIO where the DS18B20 sensors are connected to
const int oneWireBus = 4;
// Setup a oneWire instance to communicate with OneWire devices (DS18B20)
OneWire oneWire(oneWireBus);
// Pass our oneWire reference to Dallas Temperature sensor
DallasTemperature sensors(&oneWire);
// Address of each sensor
DeviceAddress sensor3 = { 0x28, 0xFF, 0xA0, 0x11, 0x33, 0x17, 0x3, 0x96 };
DeviceAddress sensor1 = { 0x28, 0xFF, 0xB4, 0x6, 0x33, 0x17, 0x3, 0x4B };
DeviceAddress sensor2 = { 0x28, 0xFF, 0x43, 0xF5, 0x32, 0x18, 0x2, 0xA8 };
DeviceAddress sensor4 = { 0x28, 0xFF, 0x11, 0x28, 0x33, 0x18, 0x1, 0x6B };
// Get Sensor Readings and return JSON object
String getSensorReadings(){
sensors.requestTemperatures();
readings["sensor1"] = String(sensors.getTempC(sensor1));
readings["sensor2"] = String(sensors.getTempC(sensor2));
readings["sensor3"] = String(sensors.getTempC(sensor3));
readings["sensor4"] = String(sensors.getTempC(sensor4));
String jsonString = JSON.stringify(readings);
return jsonString;
}
// Initialize SPIFFS
void initSPIFFS() {
if (!SPIFFS.begin()) {
Serial.println("An error has occurred while mounting SPIFFS");
}
else{
Serial.println("SPIFFS mounted successfully");
}
}
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
// Serial port for debugging purposes
Serial.begin(115200);
initWiFi();
initSPIFFS();
// Web Server Root URL
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
server.serveStatic("/", SPIFFS, "/");
// Request for the latest sensor readings
server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
String json = getSensorReadings();
request->send(200, "application/json", json);
json = String();
});
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
// send event with message "hello!", id current millis
// and set reconnect delay to 1 second
client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);
// Start server
server.begin();
}
void loop() {
if ((millis() - lastTime) > timerDelay) {
// Send Events to the client with the Sensor Readings Every 10 seconds
events.send("ping",NULL,millis());
events.send(getSensorReadings().c_str(),"new_readings" ,millis());
lastTime = millis();
}
}
View raw code
How the code works
Let's take a look at the code and see how it works to send readings to the client using server-sent events.
Including Libraries
The OneWire and DallasTemperature libraries are needed to interface with the DS18B20 temperature sensors.
#include <OneWire.h>
#include <DallasTemperature.h>
The WiFi, ESPAsyncWebServer and AsyncTCP libraries are used to create the web server.
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
The HTML, CSS, and JavaScript files to build the web page are saved on the ESP32 filesystem (SPIFFS). So, we also need to include the SPIFFS library.
#include "SPIFFS.h"
You also need to include the Arduino_JSON library to make it easier to handle JSON strings.
#include <Arduino_JSON.h>
Network Credentials
Insert your network credentials in the following variables, so that the ESP32 can connect to your local network using Wi-Fi.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
AsyncWebServer and AsyncEventSource
Create an AsyncWebServer object on port 80.
AsyncWebServer server(80);
The following line creates a new event source on /events.
AsyncEventSource events("/events");
Declaring Variables
The readings variable is a JSON variable to hold the sensor readings in JSON format.
JSONVar readings;
The lastTime and the timerDelay variables will be used to update sensor readings every X number of seconds. As an example, we'll get new sensor readings every 30 seconds (30000 milliseconds). You can change that delay time in the timerDelay variable.
// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
DS18B20 Sensors
The DS18B20 temperature sensors are connected to GPIO 4.
// GPIO where the DS18B20 sensors are connected to
const int oneWireBus = 4;
Setup a oneWire instance to communicate with OneWire devices (DS18B20):
OneWire oneWire(oneWireBus);
Pass our oneWire reference to Dallas Temperature sensor
DallasTemperature sensors(&oneWire);
Insert the addresses of your DS18B20 Sensors in the following lines (check this section if you don't have the addresses of your sensors):
// Address of each sensor
DeviceAddress sensor3 = { 0x28, 0xFF, 0xA0, 0x11, 0x33, 0x17, 0x3, 0x96 };
DeviceAddress sensor1 = { 0x28, 0xFF, 0xB4, 0x6, 0x33, 0x17, 0x3, 0x4B };
DeviceAddress sensor2 = { 0x28, 0xFF, 0x43, 0xF5, 0x32, 0x18, 0x2, 0xA8 };
DeviceAddress sensor4 = { 0x28, 0xFF, 0x11, 0x28, 0x33, 0x18, 0x1, 0x6B };
Get DS18B20 Readings
To get readings from the DS18B20 temperature sensors, first, you need to call the requesTemperatures() method on the sensors object. Then, use the getTempC() function and pass as argument the address of the sensor you want to get the temperaturethis gets the temperature in celsius degrees.
Note: if you want to get the temperature in Fahrenheit degrees, use the getTemF() function instead.
Finally, save the readings in a JSON string (jsonString variable) and return that variable.
// Get Sensor Readings and return JSON object
String getSensorReadings(){
sensors.requestTemperatures();
readings["sensor1"] = String(sensors.getTempC(sensor1));
readings["sensor2"] = String(sensors.getTempC(sensor2));
readings["sensor3"] = String(sensors.getTempC(sensor3));
readings["sensor4"] = String(sensors.getTempC(sensor4));
String jsonString = JSON.stringify(readings);
return jsonString;
}
Initialize SPIFFS
The initSPIFFS() function initializes the SPIFFS filesystem:
// Initialize SPIFFS
void initSPIFFS() {
if (!SPIFFS.begin()) {
Serial.println("An error has occurred while mounting SPIFFS");
}
else{
Serial.println("SPIFFS mounted successfully");
}
}
Intialize WiFi
The initWiFi() function initializes Wi-Fi and prints the IP address on the Serial Monitor.
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
setup()
In the setup(), initialize the Serial Monitor, Wi-Fi and filesystem.
Serial.begin(115200);
initWiFi();
initSPIFFS();
Handle Requests
When you access the ESP32 IP address on the root / URL, send the text that is stored on the index.html file to build the web page.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
Serve the other static files requested by the client (style.css and script.js).
server.serveStatic("/", SPIFFS, "/");
Send the JSON string with the current sensor readings when you receive a request on the /readings URL.
// Request for the latest sensor readings
server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
String json = getSensorReadings();
request->send(200, "application/json", json);
json = String();
});
The json variable holds the return from the getSensorReadings() function. To send a JSON string as response, the send() method accepts as first argument the response code (200), the second is the content type (application/json) and finally the content (json variable).
Server Event Source
Set up the event source on the server.
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
// send event with message "hello!", id current millis
// and set reconnect delay to 1 second
client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);
Finally, start the server.
server.begin();
loop()
In the loop(), send events to the browser with the newest sensor readings to update the web page every 30 seconds.
if ((millis() - lastTime) > timerDelay) {
// Send Events to the client with the Sensor Readings Every 10 seconds
events.send("ping",NULL,millis());
events.send(getSensorReadings().c_str(),"new_readings" ,millis());
lastTime = millis();
}
Use the send() method on the events object and pass as an argument the content you want to send and the name of the event. In this case, we want to send the JSON string returned by the getSensorReadings() function. The name of the events is new_readings.
Uploading Code and Files
After inserting your network credentials, save the code. Go to Sketch > Show Sketch Folder, and create a folder called data.
Inside that folder you should save the HTML, CSS and JavaScript files.
Then, upload the code to your ESP32 board. Make sure you have the right board and COM port selected. Also, make sure you've added your networks credentials and the sensors' addresses to the code.
After uploading the code, you need to upload the files. Go to Tools > ESP32 Data Sketch Upload and wait for the files to be uploaded.
When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the ESP32 EN/RST button, and it should print the ESP32 IP address.
Demonstration
Open your browser and type the ESP32 IP address. You should get access to the web page that shows the sensor readings. Wait some time until it gathers some data points.
You can select a point to see its value and timestamp.
Wrapping Up
In this tutorial you've learned how to create charts with multiple series to display temperature from multiple DS18B20 sensors. You can modify this project to create as many charts and series as you want and plot data from any other sensors or sources.
Setting a Custom Hostname (Arduino IDE)
By default, the hostname of the ESP32 is espressif. In this guide, you'll learn how to set a custom hostname for your board.
To set a custom hostname for your board, call WiFi.setHostname(YOUR_NEW_HOSTNAME); before WiFi.begin();
Setting an ESP32 Hostname
The default ESP32 hostname is espressif.
There is a method provided by the WiFi.h library that allows you to set a custom hostname.
First, start by defining your new hostname. For example:
String hostname = "ESP32 Node Temperature";
Then, call the WiFi.setHostname() function before calling WiFi.begin(). You also need to call WiFi.config() as shown below:
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
WiFi.setHostname(hostname.c_str()); //define hostname
You can copy the complete example below:
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-set-custom-hostname-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
// Replace with your network credentials (STATION)
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
String hostname = "ESP32 Node Temperature";
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
WiFi.setHostname(hostname.c_str()); //define hostname
//wifi_station_set_hostname( hostname.c_str() );
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
initWiFi();
Serial.print("RRSI: ");
Serial.println(WiFi.RSSI());
}
void loop() {
// put your main code here, to run repeatedly:
}
View raw code
You can use this previous snippet of code in your projects to set a custom hostname for the ESP32.
Important: you may need to restart your router for the changes to take effect.
After this, if you go to your router settings, you'll see the ESP32 with the custom hostname.
Wrapping Up
In this tutorial, you've learned how to set up a custom hostname for your ESP32. This can be useful to identify the devices connected to your network easily. For example, if you have multiple ESP32 boards connected simultaneously, it will be easier to identify them if they have a custom hostname.
For more Wi-Fi related functions, we recommend reading the following tutorial:
ESP32 Useful Wi-Fi Library Functions (Arduino IDE)
We hope you've found this tutorial useful.
Neopixel Status Indicator and Sensor PCB Shield with Wi-Fi Manager
In this project, we'll create a status indicator PCB shield for the ESP32 featuring two rows of addressable RGB neopixel LEDs, a BME280 sensor, and a pushbutton. We'll program the board to display a web server with the BME280 sensor readings and show the temperature and humidity range on the LEDs (like two progress bars). We'll also set up a Wi-Fi Managerthe LEDs indicate whether it is already connected to a Wi-Fi network or if it is set in access point mode.
The Wi-Fi Manager allows you to connect the ESP32 boards to different Access Points (networks) without having to hard-code network credentials (SSID and password) and upload new code to the board. Your ESP will automatically join the last saved network or set up an Access Point that you can use to configure the network credentials.
By following this project, you'll learn more about the following concepts:
Controlling two addressable RGB LED strips individually with the ESP32;
Build a Web Server with the ESP32 using Server-sent Events (SSE);
Handle HTML input fields to save data on your board (SSID and password);
Save variables permanently using files on the filesystem;
Build your own Wi-Fi Manager using the ESPAsyncWebServer library;
Switch between station mode and access point mode;
And much more
To better understand how this project works, we recommend taking a look at the following tutorials:
Input Data on HTML Form ESP32/ESP8266 Web Server using Arduino IDE
How to Set an ESP32 Access Point (AP) for Web Server
ESP32 Static/Fixed IP Address
ESP32 Web Server using Server-Sent Events (Update Sensor Readings Automatically)
Watch the Video Tutorial
Resources
You can find all the resources needed to build this project in the links below (or you can visit the GitHub project page):
ESP32 Code (Arduino IDE)
Gerber files
EasyEDA project to edit the PCB
Click here to download all the files
Project Overview
Before going straight to the project, let's take a look at the PCB Shield features (hardware and software).
PCB Shield Features
The shield is designed with some headers pins to stack the ESP32 board. For this reason, if you want to build and use our PCB, you need to get the same ESP32 development board. We're using the ESP32 DEVKIT DOIT V1 board (the model with 36 GPIOs).
If you want to follow this project and have a different ESP32 model, you can assemble the circuit on a breadboard, or you can modify the PCB layout and wiring to match the pinout of your ESP32 board. Throughout this project, we provide all the necessary files if you need to modify the PCB.
Additionally, you can follow this project by assembling the circuit on a breadboard if you don't want to build a PCB shield.
The shield consists of:
BME280 temperature, humidity, and pressure sensor;
Pushbutton;
Two rows of 5 addressable RGB LEDs (WS2812B).
If you replicate this project on a breadboard, instead of individual WS2812B addressable RGB LEDs, you can use addressable RGB LED strips.
PCB Shield Pin Assignment
The following table shows the pin assignment for each component on the shield:
Component |
ESP32 Pin Assignment |
BME280 |
GPIO 21 (SDA), GPIO 22 (SCL) |
Pushbutton |
GPIO 18 |
Addressable RGB LEDs (row 1) |
GPIO 27 |
Addressable RGB LEDs (row 2) |
GPIO 32 |
PCB Software Features
You can program the shield in several different ways. We'll program the ESP32 to have the following features:
Web Server
Web server to display BME280 sensor readings: temperature, humidity, and pressure. It also displays the time of the last update. The readings update automatically every 30 seconds using server-sent events. Learn more about Server-Sent Events in this project.
Visual Interface (Addressable RGB LEDs)
The RGB LEDs on the shield behave like a progress bar showing the range of temperature and humidity values. The higher the temperature, the more LEDs will be litthe same for the humidity readings. The temperature values are displayed in an orange/yellow color, and the humidity is displayed in a teal color.
Wi-Fi Manager
The Wi-Fi Manager allows you to connect the ESP32 board to different Access Points (networks) without having to hard-code network credentials (SSID and password) and upload new code to your board. Your ESP will automatically join the last saved network or set up an Access Point that you can use to configure the network credentials.
When the board is in Access Point mode (Wi-Fi Manager), all LEDs are lit in red. When the board is in station mode (Web Server with sensor readings), all LEDs are temporarily lit in green/teal color before showing the temperature and humidity range.
Testing the Circuit on a Breadboard
Before designing and building the PCB, it's important to test the circuit on a breadboard. If you don't want to make a PCB, you can still follow this project by assembling the circuit on a breadboard.
Parts Required
To assemble the circuit on a breadboard you need the following parts (the parts for the PCB are shown in a later section):
DOIT ESP32 DEVKIT V1 Board read Best ESP32 Development Boards
BME280 (4 pins)
2x WS2812B Addressable RGB LED Strips
Pushbutton*
10k Ohm resistor
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
After gathering all the parts, assemble the circuit by following the next schematic diagram:
*We ended up not using the pushbutton for this particular project, so, it is not necessary to include it in your circuit.
Designing the PCB
To design the circuit and PCB, we used EasyEDA, a browser-based software, to design PCBs. If you want to customize your PCB, you need to upload the following files:
EasyEDA project files to edit the PCB
I'm not an expert in PCB design. However, designing simple PCBs like the one we're using in this tutorial is straightforward. Designing the circuit works like in any other circuit software tool, you place some components, and you wire them together. Then, you assign each component to a footprint.
Having the parts assigned, place each component. When you're happy with the layout, make all the connections and route your PCB.
Save your project and export the Gerber files.
Note: you can grab the project files and edit them to customize the shield for your own needs.
Download Gerber .zip file
EasyEDA project to edit the PCB
Ordering the PCBs at PCBWay
This project is sponsored by PCBWay. PCBWay is a full feature Printed Circuit Board manufacturing service.
Turn your DIY breadboard circuits into professional PCBs get 10 boards for approximately $5 + shipping (which will vary depending on your country).
Once you have your Gerber files, you can order the PCB. Follow the next steps.
1. Download the Gerber files click here to download the .zip file
2. Go to PCBWay website and open the PCB Instant Quote page.
3. PCBWay can grab all the PCB details and automatically fills them for you. Use the Quick-order PCB (Autofill parameters).
4. Press the + Add Gerber file button to upload the provided Gerber files.
And that's it. You can also use the OnlineGerberViewer to check if your PCB is looking as it should.
If you aren't in a hurry, you can use the China Post shipping method to lower your cost significantly. In our opinion, we think they overestimate the China Post shipping time.
You can increase your PCB order quantity and change the solder mask color. As usual, we've ordered the Blue color.
Once you're ready, you can order the PCBs by clicking Save to Cart and complete your order.
Unboxing the PCBs
After approximately one week using the DHL shipping method, I received the PCBs at my office.
As usual, everything comes well packed, and the PCBs are really high-quality. The letters on the silkscreen are really well-printed and easy to read.
We're really satisfied with the PCBWay service. Here are some other projects we've built using the PCBWay service:
ESP32-CAM with Telegram: Take Photos, Control Outputs, Request Sensor Readings and Motion Notifications
ESP32 IoT Shield PCB with Dashboard for Outputs and Sensors
Soldering the Components
In our PCB, we've used SMD LEDs, SMD resistors, and SMD capacitors. These can be a bit difficult to solder, but the PCB looks much better. If you've never soldered SMD before, we recommend watching a few videos to learn how it's done. You can also get an SMD DIY soldering kit to practice a bit.
Here's a list of all the components needed to assemble the PCB:
DOIT ESP32 DEVKIT V1 Board (36 GPIOs)
10x SMD WS2812B addressable RGB LEDs
1x 10k Ohm SMD resistor (1206)
10x 10nF capacitors (0805)
Pushbutton (0.55 mm)
Female pin header socket (2.54 mm)
BME280 (4 pins)
Here are the soldering tools we've used:
TS80 mini portable soldering iron
Solder 60/40 0.5mm diameter
Soldering mat
Start by soldering the SMD components. Then, solder the header pins. And finally, solder the other components or use header pins if you don't want to connect the components permanently.
Here's how the ESP32 Shield looks like after assembling all the parts.
The ESP32 board should stack perfectly on the header pins on the other side of the PCB.
Programming the Shield
As mentioned previously, we'll program the board to have the following features:
Web Server to display BME280 sensor readings (station mode);
Visual Interface (Addressable RGB LEDs): the RGB LEDs on the shield behave like two progress bars showing the range of temperature and humidity values;
Wi-Fi Manager: the ESP32 will automatically join the last saved network or set up an Access Point that you can use to configure the network credentials.
The following diagram summarizes how the project works.
When the ESP first starts, it tries to read the ssid.txt, pass.txt, and ip.txt files (1);
If the files are empty (2) (the first time you run the board, the files are empty), your board is set as an access point, and all LEDs are light up in red color (3);
Using any Wi-Fi enabled device with a browser, you can connect to the newly created Access Point (default name ESP-WIFI-MANAGER);
After establishing a connection with the ESP-WIFI-MANAGER, you can go to the default IP address 192.168.4.1 to open a web page that allows you to configure your SSID and password (4);
The SSID, password, and IP address submitted on the form are saved on the corresponding files: ssid.txt, pass.txt, and ip.txt (5);
After that, the ESP board restarts (6);
This time, after restarting, the files are not empty, so the ESP will try to connect to the Wi-Fi network in station mode using the settings you've inserted on the form (7);
If it establishes a connection, the process is completed successfully (8) (all the LEDs are temporarily lit in green/teal color);
You can access the main web page that shows sensor readings (9), and the LEDs are light up accordingly to the temperature and humidity range (10). Otherwise, it will set the Access Point (3), and you can access the default IP address (192.168.4.1) to add another SSID/password combination.
Prerequisites
We'll program the ESP32 board using Arduino IDE. So make sure you have the ESP32 board add-on installed.
Installing ESP32 Board in Arduino IDE (Windows, Mac OS X, and Linux)
If you want to program the ESP32/ESP8266 using VS Code + PlatformIO, follow the next tutorial:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
Installing Libraries (Arduino IDE)
For this project, you need to install all these libraries in your Arduino IDE.
Adafruit Neopixel (Arduino Library Manager)
Adafruit_BME280 library (Arduino Library Manager)
Adafruit_Sensor library (Arduino Library Manager)
Arduino_JSON library by Arduino version 0.1.0 (Arduino Library Manager)
ESPAsyncWebServer (.zip folder)
AsyncTCP (.zip folder)
You can install the first four libraries using the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the library name.
The ESPAsyncWebServer and AsynTCP libraries aren't available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch > Include Library > Add .zip Library and select the libraries you've just downloaded.
Installing Libraries (VS Code + PlatformIO)
If you're programming the ESP32 using PlatformIO, you should include the libraries on the platformio.ini file like this:
[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
monitor_speed = 115200
lib_deps = adafruit/Adafruit NeoPixel @ ^1.7.0
adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/Adafruit BME280 Library @ ^2.1.2
arduino-libraries/Arduino_JSON @ 0.1.0
Filesystem Uploader
Before proceeding, you need to have the ESP32 Uploader Plugin installed in your Arduino IDE. Follow the next tutorial before proceeding:
Install ESP32 Filesystem Uploader in Arduino IDE
If you're using VS Code with PlatformIO, follow the next tutorial to learn how to upload files to the filesystem:
ESP32 with VS Code and PlatformIO: Upload Files to Filesystem (SPIFFS)
Organizing your Files
To keep the project organized and make it easier to understand, we'll create five different files to build the web server:
Arduino sketch that handles the web server;
index.html: to define the content of the web page in station mode to display sensor readings;
style.css: to style the web page;
script.js: to program the behavior of the web pagehandle web server responses, events, update the time, etc.;
wifimanager.html: to define the web page's content to display the Wi-Fi Manager when the ESP32 is in access point mode.
You should save the HTML, CSS, and JavaScript files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We'll upload these files to the ESP32 filesystem (SPIFFS).
You can download all project files:
Download All the Arduino Project Files
Creating the HTML Files
For this project, you need two HTML files. One to build the main page that displays the sensor readings (index.html) and another to build the Wi-Fi Manager page (wifimanager.html).
index.html
Copy the following to the index.html file.
<!DOCTYPE html>
<html>
<head>
<title>ESP IOT DASHBOARD</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div>
<h1>ESP WEB SERVER SENSOR READINGS</h2>
</div>
<div>
<div>
<div>
<p>BME280 Sensor Readings</p>
<p>
<table>
<tr>
<th>READING</th>
<th>VALUE</th>
</tr>
<tr>
<td>Temperature</td>
<td><span></span> °C</td>
</tr>
<tr>
<td>Humidity</td>
<td><span></span> %</td>
</tr>
<tr>
<td>Pressure</td>
<td><span></span> hPa</td>
</tr>
</table>
</p>
<p>Last update: <span></span></p>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
View raw code
This file creates a table to display the sensor readings. Here's the paragraph that displays the table:
<p>
<table>
<tr>
<th>READING</th>
<th>VALUE</th>
</tr>
<tr>
<td>Temperature</td>
<td><span></span> °C</td>
</tr>
<tr>
<td>Humidity</td>
<td><span></span> %</td>
</tr>
<tr>
<td>Pressure</td>
<td><span></span> hPa</td>
</tr>
</table>
</p>
To create a table in HTML, start with the <table> and </table> tags. This encloses the entire table. To create a row, use the <tr> and </tr> tags. The table is defined with a series of rows. Use the <tr></tr> pair to enclose each row of data. The table heading is defined using the <th> and </th> tags, and each table cell is defined using the <td> and </td> tags.
Notice that the cells to display the sensor readings have <span> tags with specific ids to manipulate them later using JavaScript to insert the updated readings. For example, the cell for the temperature value has the id temp.
<td><span></span> °C</td>
Finally, there's a paragraph to display the last time the readings were updated:
<p>Last update: <span></span></p>
There's a <span> tag with the update-time id. This will be used later to insert the date and time using JavaScript.
wifimanager.html
Copy the following to the wifimanager.html file.
<!DOCTYPE html>
<html>
<head>
<title>ESP Wi-Fi Manager</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div>
<h1>ESP Wi-Fi Manager</h2>
</div>
<div>
<div>
<div>
<form action="/" method="POST">
<p>
<label for="ssid">SSID</label>
<input type="text" id ="ssid" name="ssid"><br>
<label for="pass">Password</label>
<input type="text" id ="pass" name="pass"><br>
<label for="ip">IP Address</label>
<input type="text" id ="ip" name="ip" value="192.168.1.200">
<input type ="submit" value ="Submit">
</p>
</form>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
View raw code
This file creates an HTML form to insert the SSID and password of the network you want the ESP32 to join. You can also insert the IP address that you want your ESP32 to have. The following image shows the Wi-Fi Manager web page three input fields and a submit button.
We want to send the submitted values on the input fields to the server when we click on the Submit button.
Here's the HTML form with the three input fields:
<form action="/" method="POST">
<p>
<label for="ssid">SSID</label>
<input type="text" id ="ssid" name="ssid"><br>
<label for="pass">Password</label>
<input type="text" id ="pass" name="pass"><br>
<label for="ip">IP Address</label>
<input type="text" id ="ip" name="ip" value="192.168.1.200">
<input type ="submit" value ="Submit">
</p>
</form>
This is the input field for the SSID:
<label for="ssid">SSID</label>
<input type="text" id ="ssid" name="ssid"><br>
The input field for the password:
<label for="pass">Password</label>
<input type="text" id ="pass" name="pass"><br>
The HTML form contains different form elements. All the form elements are enclosed inside this <form> tag. It contains controls (the input fields) and labels for those controls.
Additionally, the <form> tag must include the action attribute that specifies what you want to do when the form is submitted (it redirects to the / root URL, so that we remain on the same page). In our case, we want to send that data to the server (ESP32) when the user clicks the Submit button. The method attribute specifies the HTTP method (GET or POST) used when submitting the form data. In this case, we'll use HTTP POST method.
<form action="/" method="POST">
POST is used to send data to a server to create/update a resource. The data sent to the server with POST is stored in the request body of the HTTP request. In this case, after submitting the values in the input fields, the body of the HTTP POST request would look like this:
POST /
Host: localhost
ssid: YOUR-NETWORK-SSID
pass: YOUR-PASSWORD
ip: IP-ADDRESS
The <input type=submit value=Submit> creates a submit button with the text Submit. When you click this button, the data submitted in the form is sent to the server.
<input type ="submit" value ="Submit">
And finally, there is an input field for the IP address you want to attribute to the ESP in station mode. By default, we set it to 192.168.1.200. You can set another default IP address or delete the value parameterit won't have a default value, the network automatically assigns a valid IP address to your board.
<label for="ip">IP Address</label>
<input type="text" id ="ip" name="ip" value="192.168.1.200">
Creating the CSS File
Copy the following styles to your style.css file.
html {
font-family: Arial, Helvetica, sans-serif;
display: inline-block;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
p {
font-size: 1.4rem;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
}
body {
margin: 0;
}
.content {
padding: 50px;
}
.card-grid {
max-width: 800px;
margin: 0 auto;
display: grid;
grid-gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.card {
background-color: white;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title {
font-size: 1.2rem;
font-weight: bold;
color: #034078
}
th, td {
text-align: center;
padding: 8px;
}
tr:nth-child(even) {
background-color: #f2f2f2
}
tr:hover {
background-color: #ddd;
}
th {
background-color: #50b8b4;
color: white;
}
table {
margin: 0 auto;
width: 90%
}
.update-time {
font-size: 0.8rem;
color:#1282A2;
}
View raw code
This file styles the previous web pages.
Creating the JavaScript File
Copy the following to the script.js file.
// Get current sensor readings when the page loads
window.addEventListener('load', getReadings);
//Function to add date and time of last update
function updateDateTime() {
var currentdate = new Date();
var datetime = currentdate.getDate() + "/"
+ (currentdate.getMonth()+1) + "/"
+ currentdate.getFullYear() + " at "
+ currentdate.getHours() + ":"
+ currentdate.getMinutes() + ":"
+ currentdate.getSeconds();
document.getElementById("update-time").innerHTML = datetime;
console.log(datetime);
}
// Function to get current readings on the webpage when it loads for the first time
function getReadings() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var myObj = JSON.parse(this.responseText);
console.log(myObj);
document.getElementById("temp").innerHTML = myObj.temperature;
document.getElementById("hum").innerHTML = myObj.humidity;
document.getElementById("pres").innerHTML = myObj.pressure;
updateDateTime();
}
};
xhr.open("GET", "/readings", true);
xhr.send();
}
// Create an Event Source to listen for events
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('new_readings', function(e) {
console.log("new_readings", e.data);
var obj = JSON.parse(e.data);
document.getElementById("temp").innerHTML = obj.temperature;
document.getElementById("hum").innerHTML = obj.humidity;
document.getElementById("pres").innerHTML = obj.pressure;
updateDateTime();
}, false);
}
View raw code
This JavaScript handles the events sent by the server and updates the sensor readings on the corresponding places. It also requests date and time whenever a new reading is available. This file also makes a request to the latest sensor readings when you open a connection with the server.
Let's take a click look at the JavaScript file and see how it works.
Get Readings
When you access the web page for the first time, it makes a request to the server to get the current sensor readings. Otherwise, we would have to wait for new sensor readings to arrive (via Server-Sent Events), which can take some time depending on the interval that you set on the server.
Add an event listener that calls the getReadings function when the web page loads.
window.addEventListener('load', getReadings);
The window object represents an open window in a browser. The addEventListener() method sets up a function to be called when a certain event happens. In this case, we'll call the getReadings function when the pages loads (load') to get the current sensor readings.
updateDateTime() function
The updateDateTime() function gets the current date and time and places it in the HTML element with the update-time id.
function updateDateTime() {
var currentdate = new Date();
var datetime = currentdate.getDate() + "/"
+ (currentdate.getMonth()+1) + "/"
+ currentdate.getFullYear() + " at "
+ currentdate.getHours() + ":"
+ currentdate.getMinutes() + ":"
+ currentdate.getSeconds();
document.getElementById("update-time").innerHTML = datetime;
console.log(datetime);
}
getReadings() function
Now, let's take a look at the getReadings function. It sends a GET request to the server on the /readings URL and handles the responsea JSON string containing the sensor readings. It also places the temperature, humidity and pressure values on the HTML elements with the corresponding ids.
function getReadings() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var myObj = JSON.parse(this.responseText);
console.log(myObj);
document.getElementById("temp").innerHTML = myObj.temperature;
document.getElementById("hum").innerHTML = myObj.humidity;
document.getElementById("pres").innerHTML = myObj.pressure;
updateDateTime();
}
};
xhr.open("GET", "/readings", true);
xhr.send();
}
Handle Events
Now, we need to handle the events sent by the server (Server-Sent Events).
Create a new EventSource object and specify the URL of the page sending the updates. In our case, it's /events.
if (!!window.EventSource) {
var source = new EventSource('/events');
Once you've instantiated an event source, you can start listening for messages from the server with addEventListener().
These are the default event listeners, as shown in the AsyncWebServer documentation.
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
Then, add an event listener for the new_readings' event.
source.addEventListener('new_readings', function(e) {
console.log("new_readings", e.data);
var obj = JSON.parse(e.data);
document.getElementById("temp").innerHTML = obj.temperature;
document.getElementById("hum").innerHTML = obj.humidity;
document.getElementById("pres").innerHTML = obj.pressure;
updateDateTime();
}, false);
When new readings are available, the ESP sends an event (new_readings') to the client with a JSON string that contains the sensor readings.
The following line prints the content of the message on the console:
console.log("new_readings", e.data);
Then, convert the data into a JSON object with the parse() method and save it in the obj variable.
var obj = JSON.parse(e.data);
The JSON string comes in the following format:
{
"temperature" : "25",
"humidity" : "50",
"pressure" : "1015"
}
You can get the temperature with obj.temperature, the humidity with obj.humidity and the pressure with obj.pressure.
The following lines put the received data into the elements with the corresponding ids (temp,hum and pres) on the web page.
document.getElementById("temp").innerHTML = obj.temperature;
document.getElementById("hum").innerHTML = obj.humidity;
document.getElementById("pres").innerHTML = obj.pressure;
Arduino Sketch
Copy the following code to your Arduino IDE or to the main.cpp file if your using PlatformIO.
/*********
Rui Santos
Complete instructions at https://RandomNerdTutorials.com/esp32-status-indicator-sensor-pcb/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
// Create an Event Source on /events
AsyncEventSource events("/events");
// Search for parameter in HTTP POST request
const char* PARAM_INPUT_1 = "ssid";
const char* PARAM_INPUT_2 = "pass";
const char* PARAM_INPUT_3 = "ip";
//Variables to save values from HTML form
String ssid;
String pass;
String ip;
// File paths to save input values permanently
const char* ssidPath = "/ssid.txt";
const char* passPath = "/pass.txt";
const char* ipPath = "/ip.txt";
IPAddress localIP;
//IPAddress localIP(192, 168, 1, 200); // hardcoded
// Set your Gateway IP address
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);
// Timer variables (check wifi)
unsigned long previousMillis = 0;
const long interval = 10000; // interval to wait for Wi-Fi connection (milliseconds)
// WS2812B Addressable RGB LEDs
#define STRIP_1_PIN 27 // GPIO the LEDs are connected to
#define STRIP_2_PIN 32 // GPIO the LEDs are connected to
#define LED_COUNT 5 // Number of LEDs
#define BRIGHTNESS 50 // NeoPixel brightness, 0 (min) to 255 (max)
Adafruit_NeoPixel strip1(LED_COUNT, STRIP_1_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel strip2(LED_COUNT, STRIP_2_PIN, NEO_GRB + NEO_KHZ800);
// Create a sensor object
Adafruit_BME280 bme; // BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)
//Variables to hold sensor readings
float temp;
float hum;
float pres;
// Json Variable to Hold Sensor Readings
JSONVar readings;
// Timer variables (get sensor readings)
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
//-----------------FUNCTIONS TO HANDLE SENSOR READINGS-----------------//
// Init BME280
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
// Get Sensor Readings
void getSensorReadings(){
temp = bme.readTemperature();
hum = bme.readHumidity();
pres= bme.readPressure()/100.0F;
}
// Return JSON String from sensor Readings
String getJSONReadings(){
readings["temperature"] = String(temp);
readings["humidity"] = String(hum);
readings["pressure"] = String(pres);
String jsonString = JSON.stringify(readings);
return jsonString;
}
//Update RGB LED colors accordingly to temp and hum values
void updateColors(){
strip1.clear();
strip2.clear();
//Number of lit LEDs (temperature)
int tempLEDs;
if (temp<=0){
tempLEDs = 1;
}
else if (temp>0 && temp<=10){
tempLEDs = 2;
}
else if (temp>10 && temp<=20){
tempLEDs = 3;
}
else if (temp>20 && temp<=30){
tempLEDs = 4;
}
else{
tempLEDs = 5;
}
//Turn on LEDs for temperature
for(int i=0; i<tempLEDs; i++) {
strip1.setPixelColor(i, strip1.Color(255, 165, 0));
strip1.show();
}
//Number of lit LEDs (humidity)
int humLEDs = map(hum, 0, 100, 1, LED_COUNT);
//Turn on LEDs for humidity
for(int i=0; i<humLEDs; i++) { // For each pixel...
strip2.setPixelColor(i, strip2.Color(25, 140, 200));
strip2.show();
}
}
//-----------------FUNCTIONS TO HANDLE SPIFFS AND FILES-----------------//
// Initialize SPIFFS
void initSPIFFS() {
if (!SPIFFS.begin(true)) {
Serial.println("An error has occurred while mounting SPIFFS");
}
else{
Serial.println("SPIFFS mounted successfully");
}
}
// Read File from SPIFFS
String readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\r\n", path);
File file = fs.open(path);
if(!file || file.isDirectory()){
Serial.println("- failed to open file for reading");
return String();
}
String fileContent;
while(file.available()){
fileContent = file.readStringUntil('\n');
break;
}
return fileContent;
}
// Write file to SPIFFS
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\r\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("- failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("- file written");
} else {
Serial.println("- frite failed");
}
}
// Initialize WiFi
bool initWiFi() {
if(ssid=="" || ip==""){
Serial.println("Undefined SSID or IP address.");
return false;
}
WiFi.mode(WIFI_STA);
localIP.fromString(ip.c_str());
if (!WiFi.config(localIP, gateway, subnet)){
Serial.println("STA Failed to configure");
return false;
}
WiFi.begin(ssid.c_str(), pass.c_str());
Serial.println("Connecting to WiFi...");
unsigned long currentMillis = millis();
previousMillis = currentMillis;
while(WiFi.status() != WL_CONNECTED) {
currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
Serial.println("Failed to connect.");
return false;
}
}
Serial.println(WiFi.localIP());
return true;
}
void setup() {
// Serial port for debugging purposes
Serial.begin(115200);
// Initialize strips
strip1.begin();
strip2.begin();
// Set brightness
strip1.setBrightness(BRIGHTNESS);
strip2.setBrightness(BRIGHTNESS);
// Init BME280 senspr
initBME();
// Init SPIFFS
initSPIFFS();
// Load values saved in SPIFFS
ssid = readFile(SPIFFS, ssidPath);
pass = readFile(SPIFFS, passPath);
ip = readFile(SPIFFS, ipPath);
/*Serial.println(ssid);
Serial.println(pass);
Serial.println(ip);*/
if(initWiFi()) {
// If ESP32 inits successfully in station mode light up all pixels in a teal color
for(int i=0; i<LED_COUNT; i++) { // For each pixel...
strip1.setPixelColor(i, strip1.Color(0, 255, 128));
strip2.setPixelColor(i, strip2.Color(0, 255, 128));
strip1.show(); // Send the updated pixel colors to the hardware.
strip2.show(); // Send the updated pixel colors to the hardware.
}
//Handle the Web Server in Station Mode
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(SPIFFS, "/index.html", "text/html");
});
server.serveStatic("/", SPIFFS, "/");
// Request for the latest sensor readings
server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
getSensorReadings();
String json = getJSONReadings();
request->send(200, "application/json", json);
json = String();
});
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
});
server.addHandler(&events);
server.begin();
}
else {
// else initialize the ESP32 in Access Point mode
// light up all pixels in a red color
for(int i=0; i<LED_COUNT; i++) { // For each pixel...
strip1.setPixelColor(i, strip1.Color(255, 0, 0));
strip2.setPixelColor(i, strip2.Color(255, 0, 0));
//strip1.setPixelColor(i, strip1.Color(128, 0, 21));
//strip2.setPixelColor(i, strip2.Color(128, 0, 21));
strip1.show(); // Send the updated pixel colors to the hardware.
strip2.show(); // Send the updated pixel colors to the hardware.
}
// Set Access Point
Serial.println("Setting AP (Access Point)");
// NULL sets an open Access Point
WiFi.softAP("ESP-WIFI-MANAGER", NULL);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
// Web Server Root URL For WiFi Manager Web Page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/wifimanager.html", "text/html");
});
server.serveStatic("/", SPIFFS, "/");
// Get the parameters submited on the form
server.on("/", HTTP_POST, [](AsyncWebServerRequest *request) {
int params = request->params();
for(int i=0;i<params;i++){
AsyncWebParameter* p = request->getParam(i);
if(p->isPost()){
// HTTP POST ssid value
if (p->name() == PARAM_INPUT_1) {
ssid = p->value().c_str();
Serial.print("SSID set to: ");
Serial.println(ssid);
// Write file to save value
writeFile(SPIFFS, ssidPath, ssid.c_str());
}
// HTTP POST pass value
if (p->name() == PARAM_INPUT_2) {
pass = p->value().c_str();
Serial.print("Password set to: ");
Serial.println(pass);
// Write file to save value
writeFile(SPIFFS, passPath, pass.c_str());
}
// HTTP POST ip value
if (p->name() == PARAM_INPUT_3) {
ip = p->value().c_str();
Serial.print("IP Address set to: ");
Serial.println(ip);
// Write file to save value
writeFile(SPIFFS, ipPath, ip.c_str());
}
//Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
}
}
request->send(200, "text/plain", "Done. ESP will restart, connect to your router and go to IP address: " + ip);
delay(3000);
// After saving the parameters, restart the ESP32
ESP.restart();
});
server.begin();
}
}
void loop() {
// If the ESP32 is set successfully in station mode...
if (WiFi.status() == WL_CONNECTED) {
//...Send Events to the client with sensor readins and update colors every 30 seconds
if (millis() - lastTime > timerDelay) {
getSensorReadings();
updateColors();
String message = getJSONReadings();
events.send(message.c_str(),"new_readings" ,millis());
lastTime = millis();
}
}
}
View raw code
How the Code Works
Let's take a look at the code and see how it works.
First, include all the necessary libraries:
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>
#include <Adafruit_BME280.h>
The following variables are used to search for the SSID, password, and IP address on the HTTP POST request made when the form is submitted.
// Search for parameter in HTTP POST request
const char* PARAM_INPUT_1 = "ssid";
const char* PARAM_INPUT_2 = "pass";
const char* PARAM_INPUT_3 = "ip";
The ssid, pass, and ip variables save the values of the SSID, password, and IP address submitted on the form.
// Variables to save values from HTML form
String ssid;
String pass;
String ip;
The SSID, password, and IP address, when submitted, are saved in files on the ESP filesystem. The following variables refer to the path of those files.
// File paths to save input values permanently
const char* ssidPath = "/ssid.txt";
const char* passPath = "/pass.txt";
const char* ipPath = "/ip.txt";
The station IP address is submitted on the Wi-Fi Manager form. However, you need to set the gateway and subnet in your code:
IPAddress localIP;
//IPAddress localIP(192, 168, 1, 200); // hardcoded
// Set your Gateway IP address
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);
WS2812B Settings
Define the pins that control the RGB LEDs. In this case, we have two individual rows connected to GPIOs 27 and 32.
#define STRIP_1_PIN 27 // GPIO the LEDs are connected to
#define STRIP_2_PIN 32 // GPIO the LEDs are connected to
Define the number of LEDs and the brightness. You can change the brightness if you want.
#define LED_COUNT 5 // Number of LEDs
#define BRIGHTNESS 20 // NeoPixel brightness, 0 (min) to 255 (max)
Finally, initialize two Adafruit_Neopixel objects to control each strip: strip1 (temperature) and strip2 (humidity):
Adafruit_NeoPixel strip1(LED_COUNT, STRIP_1_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel strip2(LED_COUNT, STRIP_2_PIN, NEO_GRB + NEO_KHZ800);
initBME()
The initBME() function initializes the BME280 sensor on the ESP32 default I2C pins:
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
Learn more about the BME280 sensor: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity).
getSensorReadings()
The getSensorReadings() functions gets temperature, humidity, and pressure from the BME280 sensor and saves the values on the temp, hum, and pres variables.
// Get Sensor Readings
void getSensorReadings(){
temp = bme.readTemperature();
hum = bme.readHumidity();
pres= bme.readPressure()/100.0F;
}
getJSONReadings()
The getJSONReadings() function returns a JSON string from the current temperature, humidity, and pressure values.
// Return JSON String from sensor Readings
String getJSONReadings(){
readings["temperature"] = String(temp);
readings["humidity"] = String(hum);
readings["pressure"] = String(pres);
String jsonString = JSON.stringify(readings);
return jsonString;
}
updateColors()
The updateColors() function lights up the RGB LEDs accordingly to the temperature and humidity values range.
First, you need to clear the strips using the clear() method:
strip1.clear();
strip2.clear();
We need to determine how many LEDs we want to light up, taking into account the temperature and humidity values. We save the number of LEDs to lighten up on the tempLEDs and humLEDs variables.
For the temperature, if the temperature is equal to or smaller than zero degrees Celsius, we light up one LED:
if (temp<=0){
tempLEDs = 1;
}
Here are the other ranges:
0<temperature=<10 > 2 LEDs
10<temperature=<20 > 3 LEDs
20<temperature=<30 > 4 LEDs
30<temperature> 5 LEDs
//Number of lit LEDs (temperature)
int tempLEDs;
if (temp<=0){
tempLEDs = 1;
}
else if (temp>0 && temp<=10){
tempLEDs = 2;
}
else if (temp>10 && temp<=20){
tempLEDs = 3;
}
else if (temp>20 && temp<=30){
tempLEDs = 4;
}
else{
tempLEDs = 5;
}
After determining how many LEDs should be lit, we need to actually light up those LEDs. To light up an LED, we can use the setPixelColor() method on the strip1 object followed by the show() method. We need a for loop to light all LEDs.
//Turn on LEDs for temperature
for(int i=0; i<tempLEDs; i++) {
strip1.setPixelColor(i, strip1.Color(255, 165, 0));
strip1.show();
}
We follow a similar procedure for the humidity. First, determine how many LEDs should be lit:
//Number of lit LEDs (humidity)
int humLEDs = map(hum, 0, 100, 1, LED_COUNT);
And finally, light up the humidity LEDs (strip2):
for(int i=0; i<humLEDs; i++) { // For each pixel...
strip2.setPixelColor(i, strip2.Color(25, 140, 200));
strip2.show();
}
initSPIFFS()
This function initializes the ESP32 SPIFFS filesystem. In this project we save the HTML, CSS and JavaScript files to build the web server pages on the filesystem. We also have the .txt files to save the SSID, password and IP address.
void initSPIFFS() {
if (!SPIFFS.begin(true)) {
Serial.println("An error has occurred while mounting SPIFFS");
}
else{
Serial.println("SPIFFS mounted successfully");
}
}
readFile()
The readFile() function reads and returns the content of a file.
String readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\r\n", path);
File file = fs.open(path);
if(!file || file.isDirectory()){
Serial.println("- failed to open file for reading");
return String();
}
String fileContent;
while(file.available()){
fileContent = file.readStringUntil('\n');
break;
}
return fileContent;
}
writeFile()
The writeFile() functions writes content to a file.
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\r\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("- failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("- file written");
} else {
Serial.println("- frite failed");
}
}
initWiFi()
The initWiFi() function returns a boolean value (either true or false) indicating if the ESP board connected successfully to a network.
First, it checks if the ssid and ip variables are empty. If they are, it won't be able to connect to a network, so it returns false.
if(ssid=="" || ip==""){
Serial.println("Undefined SSID or IP address.");
return false;
}
If that's not the case, we'll try to connect to the network using the SSID and password saved on the ssid and pass variables and set the IP address.
WiFi.mode(WIFI_STA);
localIP.fromString(ip.c_str());
if (!WiFi.config(localIP, gateway, subnet)){
Serial.println("STA Failed to configure");
return false;
}
WiFi.begin(ssid.c_str(), pass.c_str());
Serial.println("Connecting to WiFi...");
If after 10 seconds (interval variable) , it is not able to connect to Wi-Fi, it will return false.
unsigned long currentMillis = millis();
previousMillis = currentMillis;
while(WiFi.status() != WL_CONNECTED) {
currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
Serial.println("Failed to connect.");
return false;
}
}
Serial.println(WiFi.localIP());
If none of the previous conditions are met, it means that the ESP successfully connected to the network in station mode (returns true).
return true;
setup()
In the setup(), initialize the Serial Monitor.
Serial.begin(115200);
Initialize the rows of addressable RGB LEDs (strips):
// Initialize strips
strip1.begin();
strip2.begin();
Set the strips' brightness. You can change the brightness on the BRIGTHNESS variable.
// Set brightness
strip1.setBrightness(BRIGHTNESS);
strip2.setBrightness(BRIGHTNESS);
Call the initBME() function to initialize the sensor:
// Init BME280 senspr
initBME();
Initialize the filesystem:
// Init SPIFFS
initSPIFFS();
Read the files to get the previously saved SSID, password and IP address.
// Load values saved in SPIFFS
ssid = readFile(SPIFFS, ssidPath);
pass = readFile(SPIFFS, passPath);
ip = readFile(SPIFFS, ipPath);
If the ESP connects successfully in station mode (initWiFi() function returns true):
if(initWiFi()) {
Light up all LEDs in a teal color, so that we know that the ESP32 successfully connected to a Wi-Fi network:
// If ESP32 inits successfully in station mode light up all pixels in a teal color
for(int i=0; i<LED_COUNT; i++) { // For each pixel...
strip1.setPixelColor(i, strip1.Color(0, 255, 128));
strip2.setPixelColor(i, strip2.Color(0, 255, 128));
strip1.show(); // Send the updated pixel colors to the hardware.
strip2.show(); // Send the updated pixel colors to the hardware.
}
Then, we can set the commands to handle the web server requests. Send the index.html file, when you access the root URL:
//Handle the Web Server in Station Mode
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(SPIFFS, "/index.html", "text/html");
});
Send the CSS and JavaScript files requested by the HTML file (that are also saved in SPIFFS):
server.serveStatic("/", SPIFFS, "/");
When you access the web server page for the first time, it makes a request to the server on the /readings URL asking for the latest sensor readings. When that happens, send the JSON string with the readings:
// Request for the latest sensor readings
server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
getSensorReadings();
String json = getJSONReadings();
request->send(200, "application/json", json);
json = String();
});
Set up the server-sent events on the server:
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
});
server.addHandler(&events);
Finally, start the server:
server.begin();
If the ESP32 can't connect to a Wi-Fi network, the initWiFi() function returns false. In this case, light up all the LEDs in a red color, so that we know the ESP32 will be in access point mode:
for(int i=0; i<LED_COUNT; i++) { // For each pixel...
strip1.setPixelColor(i, strip1.Color(128, 0, 21));
strip2.setPixelColor(i, strip2.Color(128, 0, 21));
strip1.show(); // Send the updated pixel colors to the hardware.
strip2.show(); // Send the updated pixel colors to the hardware.
}
Set up the ESP will as an access point:
// Set Access Point
Serial.println("Setting AP (Access Point)");
// NULL sets an open Access Point
WiFi.softAP("ESP-WIFI-MANAGER", NULL);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
To set an access point, we use the softAP() method and pass as arguments the name for the access point and the password. We want the access point to be open, so we set the password to NULL. You can add a password if you want. To learn more about setting up an Access Point, read the following tutorial:
How to Set an ESP32 Access Point (AP) for Web Server
When you access the Access Point, it shows the web page to enter the network credentials on the form. So, the ESP must send the wifimanager.html file when it receives a request on the root / URL.
// Web Server Root URL For WiFi Manager Web Page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/wifimanager.html", "text/html");
});
We must also handle what happens when the form is submitted via HTTP POST request.
The following lines save the submitted values on the ssid, pass, and ip variables and save those variables on the corresponding files.
// Get the parameters submited on the form
server.on("/", HTTP_POST, [](AsyncWebServerRequest *request) {
int params = request->params();
for(int i=0;i<params;i++){
AsyncWebParameter* p = request->getParam(i);
if(p->isPost()){
// HTTP POST ssid value
if (p->name() == PARAM_INPUT_1) {
ssid = p->value().c_str();
Serial.print("SSID set to: ");
Serial.println(ssid);
// Write file to save value
writeFile(SPIFFS, ssidPath, ssid.c_str());
}
// HTTP POST pass value
if (p->name() == PARAM_INPUT_2) {
pass = p->value().c_str();
Serial.print("Password set to: ");
Serial.println(pass);
// Write file to save value
writeFile(SPIFFS, passPath, pass.c_str());
}
// HTTP POST ip value
if (p->name() == PARAM_INPUT_3) {
ip = p->value().c_str();
Serial.print("IP Address set to: ");
Serial.println(ip);
// Write file to save value
writeFile(SPIFFS, ipPath, ip.c_str());
}
//Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
}
}
After submitting the form, send a response with some text, so that we know that the ESP received the form details:
request->send(200, "text/plain", "Done. ESP will restart, connect to your router and go to IP address: " + ip);
After three seconds, restart the ESP board with ESP.restart():
delay(3000);
// After saving the parameters, restart the ESP32
ESP.restart();
After restarting, the board will have the SSID and password saved on the files, and it will successfully initialize in station mode.
loop()
In the loop(), check if the ESP32 is successfully connected to a wi-fi station:
if (WiFi.status() == WL_CONNECTED) {
If it is, do the following every 30 seconds (timerDelay variable):
get the latest sensor readings: call the getSensorReadings() function;
update the RGB LED colors to match the temperature and humidity values: call the updateColors() functions;
send an event to the browser with the latest sensor readings in JSON format.
if (millis() - lastTime > timerDelay) {
getSensorReadings();
updateColors();
String message = getJSONReadings();
events.send(message.c_str(),"new_readings" ,millis());
lastTime = millis();
}
Demonstration
After successfully uploading all files, you can open the Serial Monitor. If it is running the code for the first time, it will try to read the ssid.txt, pass.txt, and ip.txt files and it won't succeed because those files weren't created yet. So, it will start an Access Point.
All the LEDs on the shield will be lit in red, indicating that the board is set as an access point.
On your computer or smartphone, go to your network settings and connect to the ESP-WIFI-MANAGER access point.
Then, open your browser and go to 192.168.4.1. The Wi-Fi Manager web page should open.
Enter your network credentials: SSID and Password and an available IP address on your local network. After that, you'll be redirected to the following page:
At the same time, the ESP should print the following in the Serial Monitor indicating that the parameters you've inserted were successfully saved on the corresponding files:
After a few seconds, the ESP will restart. And if you've inserted the right SSID and password it will start in station mode:
All the LEDs on the shield will be lit in a teal color for 30 seconds.
This time, open a browser on your local network and insert the ESP IP address. You should get access to the web page that displays the sensor readings:
The LEDs on the shield will light up accordingly to the temperature and humidity range.
Note: the previous picture and the web server print screen were taken at different times (that's why the values on the table don't match the number of lit LEDs). You can also watch the video demonstration.
Wrapping Up
In this tutorial, you've learned how to create a shield for the ESP32 with a BME280 sensor, two rows of addressable RGB LEDs, and a pushbutton. You've also learned how to set up a Wi-Fi Manager for your web server projects. With the Wi-Fi Manager, you can easily connect your ESP web servers to different networks without having to hard-code network credentials. You can apply the Wi-Fi Manager to any web server project.
OTA (Over-the-Air) Updates AsyncElegantOTA (VS Code + PlatformIO)
In this guide, you'll learn how to do over-the-air (OTA) updates to your ESP32 boards using the AsyncElegantOTA library and VS Code with PlatformIO. The Async Elegant OTA library creates a web server that allows you to update new firmware (a new sketch) to your board without the need to make a serial connection between the ESP32 and your computer.
Additionally, with this library, you can also upload new files to the ESP32 filesystem (SPIFFS). The library is very easy to use and it's compatible with the ESPAsyncWebServer library that we use often to build web server projects.
By the end of this tutorial, you'll be able to easily add OTA capabilities to your web server projects with the ESP32 to upload new firmware and files to the filesystem wirelessly in the future.
Recommended reading: Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266
We have a similar tutorial for the ESP8266 NodeMCU board: ESP8266 NodeMCU OTA (Over-the-Air) Updates AsyncElegantOTA (VS Code + PlatformIO)
Overview
This tutorial covers:
Add the ElegantOTA feature to your ESP32 web server
Upload new firmware via OTA to ESP32 board
Upload files to SPIFFS via OTA to ESP32 board
We recommend that you follow all the steps in this tutorial to understand how ElegantOTA works and how you can use it in your projects. To demonstrate how to do this, we'll upload files to build different web server projects.
ESP32 OTA (Over-the-Air) Programming
OTA (Over-the-Air) update is the process of loading new firmware to the ESP32 board using a Wi-Fi connection rather than a serial communication. This functionality is extremely useful in case of no physical access to the ESP32 board.
There are different ways to perform OTA updates. In this tutorial, we'll cover how to do that using the AsyncElegantOTA library. In our opinion, this is one of the best and easiest ways to perform OTA updates.
The AsyncElegantOTA library creates a web server that you can access on your local network to upload new firmware or files to the filesystem (SPIFFS). The files you upload should be in .bin format. We'll show you later in the tutorial how to get your files to .bin format.
The only disadvantage of OTA programming is that you need to add the code for OTA in every sketch you upload so that you're able to use OTA in the future. In the case of the AsyncElegantOTA library, it consists of just three lines of code.
AsyncElegantOTA Library
As mentioned previously, there are different alternatives for OTA programming with the ESP32 boards. For example, in the Arduino IDE, under the Examples folder, there is the BasicOTA example (that never worked well for us); the OTA Web Updater (works well, but it is difficult to integrate with web servers using the ESPAsyncWebServer library); and many other examples from different libraries.
Most of our web server projects with the ESP32 use the ESPAsyncWebServer library. So, we wanted a solution that was compatible with that library. The AsyncElegantOTA library is just perfect for what we want:
It is compatible with the ESPAsyncWebServer library;
You just need to add three lines of code to add OTA capabilities to your regular Async Web Server;
It allows you to update not only new firmware to the board, but also files to the ESP32 filesystem (SPIFFS);
It provides a beautiful and modern web server interface;
It works extremely well.
If you like this library and you'll use it in your projects, consider supporting the developer's work.
OTA Updates with AsyncElegantOTA Library Quick Summary
To add OTA capabilities to your projects using the AsyncElegantOTA library, follow these steps:
Iclude the AsyncElegantOTA, AsyncTCP and ESPAsyncWebServer libraries in the platformio.ini file of your project;
Include AsyncElegantOTA library at the top of the code: #include <AsyncElegantOTA.h>;
Add this line AsyncElegantOTA.begin(&server); before server.begin();
Open your browser and go to http://<IPAddress>/update, where <IPAddress> is your ESP32 IP address.
Continue reading the tutorial for more detailed steps.
How does OTA Web Updater Work?
The first sketch should be uploaded via the serial port. This sketch should contain the code to create the OTA Web Updater so that you are able to upload code later using your browser.
The OTA Web Updater sketch creates a web server you can access to upload a new sketch via a web browser.
Then, you need to implement OTA routines in every sketch you upload, so that you're able to do the next updates/uploads over-the-air.
If you upload a code without an OTA routine you'll no longer be able to access the web server and upload a new sketch over-the-air.
Install AsyncElegantOTA Library (VS Code + PIO)
In this tutorial, we'll use VS Code + PIO to program the ESP32. If you want to use Arduino IDE, follow the next tutorial: ESP32 OTA (Over-the-Air) Updates AsyncElegantOTA using Arduino IDE.
To use the AsyncElegantOTA library, include it in your platformio.ini file. You also need to include the ESPAsyncWebServer library. Add these libraries as follows:
lib_deps = ESP Async WebServer
ayushsharma82/AsyncElegantOTA @ ^2.2.5
AsyncElegantOTA ESP32 Basic Example
Let's start with the basic example provided by the library. This example creates a simple web server with the ESP32. The root URL displays some text, and the /update URL displays the interface to update the firmware and the filesystem.
Edit your platformio.ini file so that it looks as follows:
[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
monitor_speed = 115200
lib_deps = ESP Async WebServer
ayushsharma82/AsyncElegantOTA @ ^2.2.5
Copy the following code to the main.cpp file.
/*
Rui Santos
Complete project details
- Arduino IDE: https://RandomNerdTutorials.com/esp32-ota-over-the-air-arduino/
- VS Code: https://RandomNerdTutorials.com/esp32-ota-over-the-air-vs-code/
This sketch shows a Basic example from the AsyncElegantOTA library: ESP32_Async_Demo
https://github.com/ayushsharma82/AsyncElegantOTA
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
AsyncWebServer server(80);
void setup(void) {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Hi! I am ESP32.");
});
AsyncElegantOTA.begin(&server); // Start ElegantOTA
server.begin();
Serial.println("HTTP server started");
}
void loop(void) {
}
View raw code
Insert your network credentials and the code should work straight away:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
How the Code Works
First, include the necessary libraries:
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
Insert your network credentials in the following variables so that the ESP32 can connect to your local network.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Create an AsyncWebServer object on port 80:
AsyncWebServer server(80);
In the setup(), initialize the Serial Monitor:
Serial.begin(115200);
Initialize Wi-Fi:
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
Then, handle the client requests. The following lines, send some text Hi! I am ESP32. when you access the root (/) URL:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Hi! I am ESP32.");
});
If your web server needs to handle more requests you can add them (we'll show you in the next example).
Then, add the next line to start ElegantOTA:
AsyncElegantOTA.begin(&server); // Start ElegantOTA
Finally, initialize the server:
server.begin();
Access the Web Server
After uploading code to the board, open the Serial Monitor at a baud rate of 115200. Press the ESP32 on-board RST button. It should display the ESP IP address as follows (yours may be different):
In your local network, open your browser and type the ESP32 IP address. You should get access the root (/) web page with some text displayed.
Now, imagine that you want to modify your web server code. To do that via OTA, go to the ESP IP address followed by /update. The following web page should load.
Follow the next sections to learn how to upload new firmware using the AsyncElegantOTA.
Upload New Firmware OTA (Over-the-Air) Updates ESP32
Every file that you upload via OTA should be in .bin format. VS Code automatically generates the .bin file for your project when you compile the code. The file is called firmware.bin and it is saved on your project folder on the following path (or similar depending on the board you're using):
.pio/build/esp32doit-devkit-v1/firmware.bin
That's that .bin file you should upload using the AsyncElegantOTA web page if you want to upload new firmware.
Upload a New Web Server Sketch
Let's see a practical example. Imagine that after uploading the previous sketch, you want to upload a new one that allows you to control an LED via a web interface like this project. Here's the steps you need to follow:
1. Copy the following code to your main.cpp file. Don't forget to insert your network credentials.
/*
Rui Santos
Complete project details
- Arduino IDE: https://RandomNerdTutorials.com/esp32-ota-over-the-air-arduino/
- VS Code: https://RandomNerdTutorials.com/esp32-ota-over-the-air-vs-code/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
// Import required libraries
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
bool ledState = 0;
const int ledPin = 2;
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style>
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
h2{
font-size: 1.5rem;
font-weight: bold;
color: #143642;
}
.topnav {
overflow: hidden;
background-color: #143642;
}
body {
margin: 0;
}
.content {
padding: 30px;
max-width: 600px;
margin: 0 auto;
}
.card {
background-color: #F8F7F9;;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
padding-top:10px;
padding-bottom:20px;
}
.button {
padding: 15px 50px;
font-size: 24px;
text-align: center;
outline: none;
color: #fff;
background-color: #0f8b8d;
border: none;
border-radius: 5px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
/*.button:hover {background-color: #0f8b8d}*/
.button:active {
background-color: #0f8b8d;
box-shadow: 2 2px #CDCDCD;
transform: translateY(2px);
}
.state {
font-size: 1.5rem;
color:#8c8c8c;
font-weight: bold;
}
</style>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
<div>
<h1>ESP WebSocket Server</h2>
</div>
<div>
<div>
<h2>Output - GPIO 2</h2>
<p>state: <span>%STATE%</span></p>
<p><button>Toggle</button></p>
</div>
</div>
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onLoad);
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage; // <-- add this line
}
function onOpen(event) {
console.log('Connection opened');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
function onLoad(event) {
initWebSocket();
initButton();
}
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
function toggle(){
websocket.send('toggle');
}
</script>
</body>
</html>)rawliteral";
void notifyClients() {
ws.textAll(String(ledState));
}
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
if (strcmp((char*)data, "toggle") == 0) {
ledState = !ledState;
notifyClients();
}
}
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
String processor(const String& var){
Serial.println(var);
if(var == "STATE"){
if (ledState){
return "ON";
}
else{
return "OFF";
}
}
return String();
}
void setup(){
// Serial port for debugging purposes
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
// Print ESP Local IP Address
Serial.println(WiFi.localIP());
initWebSocket();
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
// Start ElegantOTA
AsyncElegantOTA.begin(&server);
// Start server
server.begin();
}
void loop() {
ws.cleanupClients();
digitalWrite(ledPin, ledState);
}
View raw code
This is the same code used in this project, but it contains the required lines of code to handle ElegantOTA:
#include <AsyncElegantOTA.h>
AsyncElegantOTA.begin(&server);
2. Edit your platformio.ini file as follows:
[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
monitor_speed = 115200
lib_deps = ESP Async WebServer
ayushsharma82/AsyncElegantOTA @ ^2.2.5
2. Save and compile your code click on the Build icon.
3. Now, in the Explorer tab of VS Code, you can check that you have a firmware.bin file under the project folder on the following path (or similar):
.pio/build/esp32doit-devkit-v1/firmware.bin
4. Now, you just need to upload that file using the ElegantOTA page. Go to your ESP IP address followed by /update. Make sure you have the firmware option selected.
5. Click on Choose File, navigate through the folder on your computer and select the file of your project.
6. Wait until the progress bar reaches 100%.
7. When it's finished, click on the Back button.
8. Then, you can go to the root (/) URL to access the new web server. This is the page that you should see when you access the ESP IP address on the root (/) URL.
You can click on the button to turn the ESP32 on-board LED on and off.
Because we've also added OTA capabilities to this new web server, we can upload a new sketch in the future if needed. You just need to go to the ESP32 IP address followed by /update.
Congratulations, you've uploaded new code to your ESP32 via Wi-Fi using ElegantOTA.
Continue reading if you want to learn how to upload files to the ESP32 filesystem (SPIFFS) using AsyncElegantOTA.
Upload Files to Filesystem OTA (Over-the-Air) Updates ESP32
In this section you'll learn to upload files to the ESP32 filesystem (SPIFFS) using AsyncElegantOTA.
Web Server with Files from SPIFFS
Imagine the scenario that you need to upload files to the ESP32 filesystem, for example: configuration files; HTML, CSS and JavaScript files to update the web server page; or any other file that you may want to save in SPIFFS via OTA.
To show you how to do this, we'll create a new web server that serves files from SPIFFS: HTML, CSS and JavaScript files to build a web page that controls the ESP32 GPIOs remotely.
Copy the following code to your main.cpp file.
/*
Rui Santos
Complete project details
- Arduino IDE: https://RandomNerdTutorials.com/esp32-ota-over-the-air-arduino/
- VS Code: https://RandomNerdTutorials.com/esp32-ota-over-the-air-vs-code/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
// Import required libraries
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>
#include <AsyncElegantOTA.h>
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
// Create a WebSocket object
AsyncWebSocket ws("/ws");
// Set number of outputs
#define NUM_OUTPUTS 4
// Assign each GPIO to an output
int outputGPIOs[NUM_OUTPUTS] = {2, 4, 12, 14};
// Initialize SPIFFS
void initSPIFFS() {
if (!SPIFFS.begin(true)) {
Serial.println("An error has occurred while mounting SPIFFS");
}
Serial.println("SPIFFS mounted successfully");
}
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
String getOutputStates(){
JSONVar myArray;
for (int i =0; i<NUM_OUTPUTS; i++){
myArray["gpios"][i]["output"] = String(outputGPIOs[i]);
myArray["gpios"][i]["state"] = String(digitalRead(outputGPIOs[i]));
}
String jsonString = JSON.stringify(myArray);
return jsonString;
}
void notifyClients(String state) {
ws.textAll(state);
}
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
if (strcmp((char*)data, "states") == 0) {
notifyClients(getOutputStates());
}
else{
int gpio = atoi((char*)data);
digitalWrite(gpio, !digitalRead(gpio));
notifyClients(getOutputStates());
}
}
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,AwsEventType type,
void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
void setup(){
// Serial port for debugging purposes
Serial.begin(115200);
// Set GPIOs as outputs
for (int i =0; i<NUM_OUTPUTS; i++){
pinMode(outputGPIOs[i], OUTPUT);
}
initSPIFFS();
initWiFi();
initWebSocket();
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html",false);
});
server.serveStatic("/", SPIFFS, "/");
// Start ElegantOTA
AsyncElegantOTA.begin(&server);
// Start server
server.begin();
}
void loop() {
ws.cleanupClients();
}
View raw code
Insert your network credentials in the following variables and save the code.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Edit your platformio.ini file so that it looks as follows:
[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
monitor_speed = 115200
lib_deps = ESP Async WebServer
arduino-libraries/Arduino_JSON @ 0.1.0
ayushsharma82/AsyncElegantOTA @ ^2.2.5
Update Firmware
After inserting your network credentials, save and compile the code.
Go to the ESP IP address followed by /update and upload the new firmware as shown previously.
Next, we'll see how to upload the files to the filesystem.
Update Filesystem
Under the project folder create a folder called data and paste the following HTML, CSS and JavaScript files (click on the links to download the files):
HTML file: index.html
CSS file: style.css
JavaScript file: script.js
Download all files
In VS Code, click on to the PIO icon and go to Project Tasks > env:esp32doit-devkit-v1 (or similar) >Platform > Build Filesystem Image. This will create a .bin file from the files saved in the data folder.
After building the filesystem image, you should have a spiffs.bin file in the following path (or similar):
.pio/build/esp32doit-devkit-v1/spiffs.bin
That's that file that you should upload to update the filesystem.
Go to your ESP IP address followed by /update. Make sure you have the Filesystem option selected and select the spiffs.bin file.
After successfully uploading, click the Back button. And go to the root (/) URL again.
You should get access to the following web page that controls the ESP32 outputs using Web Socket protocol.
To see the web server working, you can connect 4 LEDs to your ESP32 on GPIOS: 2, 4, 12 and 14. You should be able to control those outputs from the web server.
If you need to update something on your project, you just need to go to your ESP32 IP address followed by /update.
Congratulations! You've successfully uploaded files to the ESP32 filesystem using ElegantOTA.
Watch the Video Demonstration
Wrapping Up
In this tutorial, you've learned how to add OTA capabilities to your Async Web Servers using the AsyncElegantOTA library. This library is super simple to use and allows you to upload new firmware or files to the filesystem effortlessly using a web page. In our opinion, the AsyncElegantOTA library is one of the best options to handle OTA web updates.
Guide for MicroSD Card Module using Arduino IDE
This guide shows how to use a microSD card with the ESP32: you'll learn how to read and write files to the microSD card. To interface the microSD card with the ESP32 board, we'll use a microSD card module (SPI communication protocol). Using a microSD card with the ESP32 is especially useful for data logging or storing files that don't fit in the filesystem (SPIFFS). The ESP32 will be programmed using the Arduino core.
In this tutorial, we'll cover the following topics:
Introducing the MicroSD Card Module and Pinout
Interface ESP32 with microSD Card Module
Testing the microSD Card Module: read, write and handle files
Use custom SPI pins with the microSD card
ESP32 Data Logging to microSD Card (sensor readings)
ESP32 Web Server with Files from microSD Card
MicroSD Card Module
There are different microSD card modules compatible with the ESP32. We're using the microSD card module sown in the following figure it communicates using SPI communication protocol. You can use any other microSD card module with an SPI interface.
This microSD card module is also compatible with other microcontrollers like the Arduino and the ESP8266 NodeMCU boards. To learn how to use the microSD card module with the Arduino, you can follow the next tutorial:
Guide to SD Card Module with Arduino
Where to Buy?
You can click the link below to check different stores where you can get the microSD card module:
MicroSD card module
MicroSD Card Module Pinout SPI
The microSD card module communicates using SPI communication protocol. You can connect it to the ESP32 using the default SPI pins.
MicroSD card module |
ESP32 |
3V3 |
3.3V |
CS |
GPIO 5 |
MOSI |
GPIO 23 |
CLK |
GPIO 18 |
MISO |
GPIO 19 |
GND |
GND |
Parts Required
For this tutorial, you need the following parts:
ESP32 development board (read: Best ESP32 development boards)
MicroSD Card Module
MicroSD Card
Jumper Wires
Breadboard
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
ESP32 with microSD Card Module Schematic Diagram
To wire the microSD card module to the ESP32 board, you can follow the next schematic diagram (for the default ESP32 SPI pins):
Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
Preparing the microSD Card
Before proceeding with the tutorial, make sure you format your microSD card as FAT32. Follow the next instructions to format your microSD card or use a software tool like SD Card Formater (compatible with Windows and Mac OS).
1. Insert the microSD card into your computer. Go to My Computer and right-click on the SD card. Select Format as shown in the figure below.
2. A new window pops up. Select FAT32, press Start to initialize the formatting process and follow the onscreen instructions.
Preparing Arduino IDE
We'll program the ESP32 board using Arduino IDE. So, make sure you have the ESP32 add-on installed. Follow the next tutorial:
Install the ESP32 Board in Arduino IDE
If you prefer using VSCode + PlatformIO, follow the next tutorial instead:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266
ESP32 Handling Files with a MicroSD Card Module
There are two different libraries for the ESP32 (included in the Arduino core for the ESP32): the SD library and the SDD_MMC.h library.
If you use the SD library, you're using the SPI controller. If you use the SDD_MMC library you're using the ESP32 SD/SDIO/MMC controller. You can learn more about the ESP32 SD/SDIO/MMC driver.
There are several examples in Arduino IDE that show how to handle files on the microSD card using the ESP32. In the Arduino IDE, go to File > Examples > SD(esp32) > SD_Test, or copy the following code.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-microsd-card-arduino/
This sketch can be found at: Examples > SD(esp32) > SD_Test
*/
#include "FS.h"
#include "SD.h"
#include "SPI.h"
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if(!root){
Serial.println("Failed to open directory");
return;
}
if(!root.isDirectory()){
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while(file){
if(file.isDirectory()){
Serial.print(" DIR : ");
Serial.println(file.name());
if(levels){
listDir(fs, file.name(), levels -1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void createDir(fs::FS &fs, const char * path){
Serial.printf("Creating Dir: %s\n", path);
if(fs.mkdir(path)){
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %s\n", path);
if(fs.rmdir(path)){
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\n", path);
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
file.close();
}
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %s\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
Serial.println("Rename failed");
}
}
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %s\n", path);
if(fs.remove(path)){
Serial.println("File deleted");
} else {
Serial.println("Delete failed");
}
}
void testFileIO(fs::FS &fs, const char * path){
File file = fs.open(path);
static uint8_t buf[512];
size_t len = 0;
uint32_t start = millis();
uint32_t end = start;
if(file){
len = file.size();
size_t flen = len;
start = millis();
while(len){
size_t toRead = len;
if(toRead > 512){
toRead = 512;
}
file.read(buf, toRead);
len -= toRead;
}
end = millis() - start;
Serial.printf("%u bytes read for %u ms\n", flen, end);
file.close();
} else {
Serial.println("Failed to open file for reading");
}
file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
size_t i;
start = millis();
for(i=0; i<2048; i++){
file.write(buf, 512);
}
end = millis() - start;
Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
file.close();
}
void setup(){
Serial.begin(115200);
if(!SD.begin(5)){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
listDir(SD, "/", 0);
createDir(SD, "/mydir");
listDir(SD, "/", 0);
removeDir(SD, "/mydir");
listDir(SD, "/", 2);
writeFile(SD, "/hello.txt", "Hello ");
appendFile(SD, "/hello.txt", "World!\n");
readFile(SD, "/hello.txt");
deleteFile(SD, "/foo.txt");
renameFile(SD, "/hello.txt", "/foo.txt");
readFile(SD, "/foo.txt");
testFileIO(SD, "/test.txt");
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
}
void loop(){
}
View raw code
This example shows how to do almost any task you may need with the microSD card:
List a directory;
Create a directory;
Remove a directory;
Read a file content;
Write content to a file;
Append content to file;
Rename a file;
Delete a file;
Initialize microSD card;
Get microSD card type;
Get microSD card size;
Alternatively, you can use the SD_MMC examples these are similar to the SD examples, but use the SDMMC driver. For the SDMMC driver, you need a compatible microSD card module. The module we're using in this tutorial doesn't support SDMMC.
How the Code Works
First, you need to include the following libraries: FS.h to handle files, SD.h to interface with the microSD card and SPI.h to use SPI communication protocol.
#include "FS.h"
#include "SD.h"
#include "SPI.h"
The example provides several functions to handle files on the microSD card.
List a directory
The listDir() function lists the directories on the SD card. This function accepts as arguments the filesystem (SD), the main directory's name, and the levels to go into the directory.
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if(!root){
Serial.println("Failed to open directory");
return;
}
if(!root.isDirectory()){
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while(file){
if(file.isDirectory()){
Serial.print(" DIR : ");
Serial.println(file.name());
if(levels){
listDir(fs, file.name(), levels -1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
Here's an example of how to call this function. The / corresponds to the microSD card root directory.
listDir(SD, "/", 0);
Create a Directory
The createDir() function creates a new directory. Pass as an argument the SD filesystem and the directory name path.
void createDir(fs::FS &fs, const char * path){
Serial.printf("Creating Dir: %s\n", path);
if(fs.mkdir(path)){
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}
For example, the following command creates a new directory on the root called mydir.
createDir(SD, "/mydir");
Remove a Directory
To remove a directory from the microSD card, use the removeDir() function and pass as an argument the SD filesystem and the directory name path.
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %s\n", path);
if(fs.rmdir(path)){
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}
Here is an example:
removeDir(SD, "/mydir");
Read File Content
The readFile() function reads the content of a file and prints the content in the Serial Monitor. As with previous functions, pass as an argument the SD filesystem and the file path.
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\n", path);
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
file.close();
}
For example, the following line reads the content of the hello.txt file.
readFile(SD, "/hello.txt")
Write Content to a File
To write content to a file, you can use the writeFile() function. Pass as an argument, the SD filesystem, the file path and the message
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
The following line writes Hello in the hello.txt file.
writeFile(SD, "/hello.txt", "Hello ");
Append Content to a File
Similarly, you can append content to a file (without overwriting previous content) using the appendFile() function.
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
The following line appends the message World!\n in the hello.txt file. The \n means that the next time you write something to the file, it will be written in a new line.
appendFile(SD, "/hello.txt", "World!\n");
Rename a File
You can rename a file using the renameFile() function. Pass as arguments the SD filesystem, the original filename, and the new filename.
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %s\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
Serial.println("Rename failed");
}
}
The following line renames the hello.txt file to foo.txt.
renameFile(SD, "/hello.txt", "/foo.txt");
Delete a File
Use the deleteFile() function to delete a file. Pass as an argument the SD filesystem and the file path of the file you want to delete.
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %s\n", path);
if(fs.remove(path)){
Serial.println("File deleted");
} else {
Serial.println("Delete failed");
}
}
The following line deletes the foo.txt file from the microSD card.
deleteFile(SD, "/foo.txt");
Test a File
The testFileIO() functions shows how long it takes to read the content of a file.
void testFileIO(fs::FS &fs, const char * path){
File file = fs.open(path);
static uint8_t buf[512];
size_t len = 0;
uint32_t start = millis();
uint32_t end = start;
if(file){
len = file.size();
size_t flen = len;
start = millis();
while(len){
size_t toRead = len;
if(toRead > 512){
toRead = 512;
}
file.read(buf, toRead);
len -= toRead;
}
end = millis() - start;
Serial.printf("%u bytes read for %u ms\n", flen, end);
file.close();
}
else {
Serial.println("Failed to open file for reading");
}
file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
size_t i;
start = millis();
for(i=0; i<2048; i++){
file.write(buf, 512);
}
end = millis() - start;
Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
file.close();
}
The following function tests the test.txt file.
testFileIO(SD, "/test.txt");
Initialize the microSD Card
In the setup(), the following lines initialize the microSD card with SD.begin().
Serial.begin(115200);
if(!SD.begin()){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
If you don't pass any argument to the begin() function, it will try to initialize SPI communication with the microSD card on the default chip select (CS) pin. If you want to use another CS pin, you can pass it as an argument to the begin() function. For example, if you wanted to use GPIO 17 as a CS pin, you should use the following lines of code:
Serial.begin(115200);
if(!SD.begin(17)){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
If you want to use custom SPI pins with the microSD card, go to this section.
Get microSD Card Type
The following lines print the microSD card type on the Serial Monitor.
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
Get microSD Card Size
You can get the microSD card size by calling the cardSize() method:
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
Testing MicroSD Card Functions
The following lines call the functions we've seen previously.
listDir(SD, "/", 0);
createDir(SD, "/mydir");
listDir(SD, "/", 0);
removeDir(SD, "/mydir");
listDir(SD, "/", 2);
writeFile(SD, "/hello.txt", "Hello ");
appendFile(SD, "/hello.txt", "World!\n");
readFile(SD, "/hello.txt");
deleteFile(SD, "/foo.txt");
renameFile(SD, "/hello.txt", "/foo.txt");
readFile(SD, "/foo.txt");
testFileIO(SD, "/test.txt");
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
Demonstration
Upload the previous sketch to your ESP32 board. After that, open the Serial Monitor and press the ESP32 on-board RST button. If the initialization succeeds, you'll get similar messages on the Serial Monitor.
Use Custom SPI Pins with the MicroSD Card
The SD.h and SD_MMC.h libraries use the VSPI SPI pins (23, 19, 18, 5) by default. You can set other pins as SPI pins. The ESP32 features two SPI interfaces: HSPI and VSPI on the following pins:
SPI |
MOSI |
MISO |
CLK |
CS |
VSPI |
GPIO 23 |
GPIO 19 |
GPIO 18 |
GPIO 5 |
HSPI |
GPIO 13 |
GPIO 12 |
GPIO 14 |
GPIO 15 |
Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
To use other SPI pins, you can proceed as follows:
At the beginning of your code, declare the pins you want to use, for example:
#define SCK 17
#define MISO 19
#define MOSI 23
#define CS 5
Create a new SPI class on HSPI or VSPI. We're using VSPI. Both will work fine.
SPIClass spi = SPIClass(VSPI);
In the setup(), initialize SPI communication protocol on the pins defined previously:
spi.begin(SCK, MISO, MOSI, CS);
Finally, initialize the microSD card with the begin() method. Pass as argument the CS pin, the SPI instance you want to use, and the bus frequency.
if (!SD.begin(CS,spi,80000000)) {
Serial.println("Card Mount Failed");
return;
}
Here is the sample code modified to use custom SPI pins:
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-microsd-card-arduino/
This sketch was mofidied from: Examples > SD(esp32) > SD_Test
*/
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#define SCK 17
#define MISO 19
#define MOSI 23
#define CS 5
SPIClass spi = SPIClass(VSPI);
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if(!root){
Serial.println("Failed to open directory");
return;
}
if(!root.isDirectory()){
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while(file){
if(file.isDirectory()){
Serial.print(" DIR : ");
Serial.println(file.name());
if(levels){
listDir(fs, file.name(), levels -1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void createDir(fs::FS &fs, const char * path){
Serial.printf("Creating Dir: %s\n", path);
if(fs.mkdir(path)){
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %s\n", path);
if(fs.rmdir(path)){
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\n", path);
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
file.close();
}
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %s\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
Serial.println("Rename failed");
}
}
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %s\n", path);
if(fs.remove(path)){
Serial.println("File deleted");
} else {
Serial.println("Delete failed");
}
}
void testFileIO(fs::FS &fs, const char * path){
File file = fs.open(path);
static uint8_t buf[512];
size_t len = 0;
uint32_t start = millis();
uint32_t end = start;
if(file){
len = file.size();
size_t flen = len;
start = millis();
while(len){
size_t toRead = len;
if(toRead > 512){
toRead = 512;
}
file.read(buf, toRead);
len -= toRead;
}
end = millis() - start;
Serial.printf("%u bytes read for %u ms\n", flen, end);
file.close();
} else {
Serial.println("Failed to open file for reading");
}
file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
size_t i;
start = millis();
for(i=0; i<2048; i++){
file.write(buf, 512);
}
end = millis() - start;
Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
file.close();
}
void setup(){
Serial.begin(115200);
spi.begin(SCK, MISO, MOSI, CS);
if (!SD.begin(CS,spi,80000000)) {
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
listDir(SD, "/", 0);
createDir(SD, "/mydir");
listDir(SD, "/", 0);
removeDir(SD, "/mydir");
listDir(SD, "/", 2);
writeFile(SD, "/hello.txt", "Hello ");
appendFile(SD, "/hello.txt", "World!\n");
readFile(SD, "/hello.txt");
deleteFile(SD, "/foo.txt");
renameFile(SD, "/hello.txt", "/foo.txt");
readFile(SD, "/foo.txt");
testFileIO(SD, "/test.txt");
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
}
void loop(){
}
View raw code
Example: ESP32 Data Logging to microSD Card
Using a microSD card is especially useful for data logging projects. As an example, we'll show you how to save sensor readings from a BME280 sensor with timestamps (epoch time).
Prerequisites
For this example, make sure you have the following libraries installed:
Adafruit BME280 Library
Adafruit Unified Sensor Driver
You can install these libraries using the Arduino library manager. In your Arduino IDE, go to Sketch > Include Library > Manage Libraries Then, search for the library names and install them.
If you're using VS Code with PlatformIO, copy the following lines to the platformio.ini file to include all the necessary libraries.
lib_deps = adafruit/Adafruit BME280 Library @ ^2.1.0
adafruit/Adafruit Unified Sensor @ ^1.1.4
Schematic Diagram
For this example, you need to wire the microSD card module and the BME280 sensor to the ESP32. Here's a list of the parts required:
ESP32 development board (read: Best ESP32 development boards)
BME280 sensor
MicroSD Card Module
MicroSD Card
Jumper Wires
Breadboard
Wire the circuit by following the next schematic diagram.
You can also take a look at the following tables:
BME280 |
ESP32 |
VIN |
3V3 |
GND |
GND |
SCL |
GPIO 22 |
SDA |
GPIO 21 |
microSD card module |
ESP32 |
3V3 |
3.3V |
CS |
GPIO 5 |
MOSI |
GPIO 23 |
CLK |
GPIO 18 |
MISO |
GPIO 19 |
GND |
GND |
Code
Copy the following code to your Arduino IDE. This sketch gets BME280 sensor readings (temperature, humidity, and pressure) and logs them in a file on the microSD card every 30 seconds. It also logs the timestamp (epoch time requested to an NTP server).
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-microsd-card-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
// Libraries for SD card
#include "FS.h"
#include "SD.h"
#include <SPI.h>
//Libraries for BME280 sensor
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// Libraries to get time from NTP Server
#include <WiFi.h>
#include "time.h"
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
// BME280 I2C
Adafruit_BME280 bme;
// Variables to hold sensor readings
float temp;
float hum;
float pres;
String dataMessage;
// NTP server to request epoch time
const char* ntpServer = "pool.ntp.org";
// Variable to save current epoch time
unsigned long epochTime;
// Function that gets current epoch time
unsigned long getTime() {
time_t now;
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
//Serial.println("Failed to obtain time");
return(0);
}
time(&now);
return now;
}
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
// Init BME280
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
// Initialize SD card
void initSDCard(){
if (!SD.begin()) {
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
}
// Write to the SD card
void writeFile(fs::FS &fs, const char * path, const char * message) {
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file) {
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)) {
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
// Append data to the SD card
void appendFile(fs::FS &fs, const char * path, const char * message) {
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file) {
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)) {
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
void setup() {
Serial.begin(115200);
initWiFi();
initBME();
initSDCard();
configTime(0, 0, ntpServer);
// If the data.txt file doesn't exist
// Create a file on the SD card and write the data labels
File file = SD.open("/data.txt");
if(!file) {
Serial.println("File doesn't exist");
Serial.println("Creating file...");
writeFile(SD, "/data.txt", "Epoch Time, Temperature, Humidity, Pressure \r\n");
}
else {
Serial.println("File already exists");
}
file.close();
}
void loop() {
if ((millis() - lastTime) > timerDelay) {
//Get epoch time
epochTime = getTime();
//Get sensor readings
temp = bme.readTemperature();
//temp = 1.8*bme.readTemperature() + 32;
hum = bme.readHumidity();
pres = bme.readPressure()/100.0F;
//Concatenate all info separated by commas
dataMessage = String(epochTime) + "," + String(temp) + "," + String(hum) + "," + String(pres)+ "\r\n";
Serial.print("Saving data: ");
Serial.println(dataMessage);
//Append the data to file
appendFile(SD, "/data.txt", dataMessage.c_str());
lastTime = millis();
}
}
View raw code
Insert your network credentials in the following variables and the code will work straight away:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
This example uses the functions we've seen previously to write and append data to the microSD card (writeFile() and appendFile() functions).
To better understand how this example works, we recommend taking a look at the following tutorials:
Get Epoch/Unix Time with the ESP32 (Arduino)
ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity)
Demonstration
Upload the code to your board. You can check on the Serial Monitor if everything is working as expected.
Let the project run for a while to gather some readings. Then, insert the microSD card on your computer, and you should have a file called data.txt with the sensor readings.
Example: ESP32 Web Server with Files from microSD Card
You can save the files to build a web server with the ESP32 on a microSD card (HTML, CSS, JavaScript, and image files). This can be useful if the files are too big to fit on the ESP32 filesystem, or it can also be more convenient depending on your project.
To show you how to do this, we'll create a simple web server that serves a HTML, a CSS and a PNG file to build a web page and display a favicon.
Move the following files to your microSD card (click on the links to download the files):
HTML file
CSS file
PNG file
Note: move only the files to the microSD card, not the folders.
Then, upload the following code to your Arduino IDE.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-web-server-microsd-card/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
void initSDCard(){
if(!SD.begin()){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
}
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
initWiFi();
initSDCard();
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SD, "/index.html", "text/html");
});
server.serveStatic("/", SD, "/");
server.begin();
}
void loop() {
}
View raw code
Insert your network credentials in the following variables, and the code should work straight away:
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Demonstration
After uploading the files to the microSD card and the sketch to your ESP32 board, open the Serial Monitor at a baud rate of 115200. Press the ESP32 on-board RST button. The ESP32 IP address will be displayed on the Serial Monitor.
On your local network, open a web browser and type the ESP32 IP address. You should get access to the following web page built with the files stored on the microSD card.
For a detailed explanation of this project, refer to the following tutorial:
ESP32 Web Server with Files from microSD Card
Wrapping Up
In this tutorial, you've learned how to interface a microSD card with the ESP32 and read and write files. You've also learned how to use it for data logging projects or storing files to serve in your web server projects.
OTA (Over-the-Air) Updates AsyncElegantOTA using Arduino IDE
In this guide, you'll learn how to do over-the-air (OTA) updates to your ESP32 boards using the AsyncElegantOTA library. This library creates a web server that allows you to upload new firmware (a new sketch) to your board without the need to make a serial connection between the ESP32 and your computer.
Additionally, with this library, you can also upload new files to the ESP32 filesystem (SPIFFS). The library is very easy to use, and it's compatible with the ESPAsyncWebServer library that we often use to build web server projects.
By the end of this tutorial, you'll be able to easily add OTA capabilities to your web server projects with the ESP32 to upload new firmware and files to the filesystem wirelessly in the future.
We have a similar tutorial for the ESP8266 NodeMCU board: ESP8266 NodeMCU OTA (Over-the-Air) Updates AsyncElegantOTA using Arduino IDE
Watch the Video Tutorial
This project is available in video format and in written format. You can watch the video below or you can scroll down for the written instructions.
Overview
This tutorial covers:
Add the ElegantOTA feature to your web server
Upload new firmware via OTA to ESP32 board
Upload files to SPIFFS via OTA to ESP32 board
We recommend that you follow all the tutorial steps to understand how ElegantOTA works and how you can use it in your projects. To demonstrate how to do this, we'll upload files to build different web server projects.
ESP32 OTA (Over-the-Air) Programming
OTA (Over-the-Air) update is the process of loading new firmware to the ESP32 board using a Wi-Fi connection rather than a serial communication. This functionality is extremely useful in case of no physical access to the ESP32 board.
There are different ways to perform OTA updates. In this tutorial, we'll cover how to do that using the AsyncElegantOTA library. In our opinion, this is one of the best and easiest ways to perform OTA updates.
The AsyncElegantOTA library creates a web server that you can access on your local network to upload new firmware or files to the filesystem (SPIFFS). The files you upload should be in .bin format. We'll show you later in the tutorial how to convert your files to .bin format.
The only disadvantage of OTA programming is that you need to add the code for OTA in every sketch you upload so that you're able to use OTA in the future. In the case of the AsyncElegantOTA library, it consists of just three lines of code.
AsyncElegantOTA Library
As mentioned previously, there are a bunch of alternatives for OTA programming with the ESP32 boards. For example, in the Arduino IDE, under the Examples folder, there is the BasicOTA example (that never worked well for us); the OTA Web Updater (works well, but it isn't easy to integrate with web servers using the ESPAsyncWebServer library); and many other examples from different libraries.
Most of our web server projects with the ESP32 use the ESPAsyncWebServer library. So, we wanted a solution that was compatible with that library. The AsyncElegantOTA library is just perfect for what we want:
It is compatible with the ESPAsyncWebServer library;
You just need to add three lines of code to add OTA capabilities to your regular Async Web Server;
It allows you to update not only new firmware to the board but also files to the ESP32 filesystem (SPIFFS);
It provides a beautiful and modern web server interface;
It works extremely well.
If you like this library and you'll use it in your projects, consider supporting the developer's work.
OTA Updates with AsyncElegantOTA Library Quick Summary
To add OTA capabilities to your projects using the AsyncElegantOTA library, follow these steps:
Install AsyncElegantOTA, AsyncTCP, and ESPAsyncWebServer libraries;
Include AsyncElegantOTA library at the top of the Arduino sketch: #include <AsyncElegantOTA.h>;
Add this line AsyncElegantOTA.begin(&server); before server.begin();
Open your browser and go to http://<IPAddress>/update, where <IPAddress> is your ESP32 IP address.
Continue reading the tutorial for more detailed steps.
How does OTA Web Updater Work?
The first sketch should be uploaded via serial port. This sketch should contain the code to create the OTA Web Updater so that you are able to upload code later using your browser.
The OTA Web Updater sketch creates a web server you can access to upload a new sketch via web browser.
Then, you need to implement OTA routines in every sketch you upload so that you're able to do the next updates/uploads over-the-air.
If you upload a code without an OTA routine, you'll no longer be able to access the web server and upload a new sketch over-the-air.
Install AsyncElegantOTA Library
In this tutorial, the ESP32 will be programmed using Arduino IDE. If you want to learn how to do the same using VS Code + PlatformIO, follow the next tutorial: ESP32 OTA (Over-the-Air) Updates AsyncElegantOTA (VS Code + PlatformIO)
You can install the AsyncElegantOTA library using the Arduino Library Manager. In your Arduino IDE, go to Sketch > Include Library > Manage Libraries Search for AsyncElegantOTA and install it.
Install AsyncTCP and ESPAsyncWebServer Libraries
You also need to install the AsyncTCP and the ESPAsyncWebServer libraries. Click the links below to download the libraries.
ESPAsyncWebServer
AsyncTCP
These libraries aren't available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch > Include Library > Add .zip Library and select the libraries you've just downloaded.
AsyncElegantOTA ESP32 Basic Example
Let's start with the basic example provided by the library. This example creates a simple web server with the ESP32. The root URL displays some text, and the /update URL displays the interface to update firmware and filesystem.
Copy the following code to your Arduino IDE.
/*
Rui Santos
Complete project details
- Arduino IDE: https://RandomNerdTutorials.com/esp32-ota-over-the-air-arduino/
- VS Code: https://RandomNerdTutorials.com/esp32-ota-over-the-air-vs-code/
This sketch shows a Basic example from the AsyncElegantOTA library: ESP32_Async_Demo
https://github.com/ayushsharma82/AsyncElegantOTA
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
AsyncWebServer server(80);
void setup(void) {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Hi! I am ESP32.");
});
AsyncElegantOTA.begin(&server); // Start ElegantOTA
server.begin();
Serial.println("HTTP server started");
}
void loop(void) {
}
View raw code
Insert your network credentials and the code should work straight away:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
How the Code Works
First, include the necessary libraries:
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
Insert your network credentials in the following variables so that the ESP32 can connect to your local network.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Create an AsyncWebServer object on port 80:
AsyncWebServer server(80);
In the setup(), initialize the Serial Monitor:
Serial.begin(115200);
Initialize Wi-Fi:
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
Then, handle the client requests. The following lines, send some text Hi! I am ESP32. when you access the root (/) URL:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Hi! I am ESP32.");
});
If your web server needs to handle more requests you can add them (we'll show you in the next example).
Then, add the next line to start ElegantOTA:
AsyncElegantOTA.begin(&server);
Finally, initialize the server:
server.begin();
Access the Web Server
After uploading code to the board, open the Serial Monitor at a baud rate of 115200. Press the ESP32 on-board RST button. It should display the ESP IP address as follows (yours may be different):
In your local network, open your browser and type the ESP32 IP address. You should get access the root (/) web page with some text displayed.
Now, imagine that you want to modify your web server code. To do that via OTA, go to the ESP IP address followed by /update. The following web page should load.
Follow the next sections to learn how to upload new firmware using AsyncElegantOTA.
Upload New Firmware OTA (Over-the-Air) Updates ESP32
Every file that you upload via OTA should be in .bin format. You can generate a .bin file from your sketch using the Arduino IDE.
With your sketch opened, you just need to go to Sketch > Export Compiled Binary. A .bin file will be generated from your sketch. The generated file will be saved under your project folder.
That's that .bin file you should upload using the AsyncElegantOTA web page if you want to upload new firmware.
Upload a New Web Server Sketch Example
Let's see a practical example. Imagine that after uploading the previous sketch, you want to upload a new one that allows you to control an LED via a web interface like this project. Here's the steps you need to follow:
1. Copy the following code to your Arduino IDE. Don't forget to insert your network credentials.
/*
Rui Santos
Complete project details
- Arduino IDE: https://RandomNerdTutorials.com/esp32-ota-over-the-air-arduino/
- VS Code: https://RandomNerdTutorials.com/esp32-ota-over-the-air-vs-code/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
// Import required libraries
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
bool ledState = 0;
const int ledPin = 2;
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style>
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
h2{
font-size: 1.5rem;
font-weight: bold;
color: #143642;
}
.topnav {
overflow: hidden;
background-color: #143642;
}
body {
margin: 0;
}
.content {
padding: 30px;
max-width: 600px;
margin: 0 auto;
}
.card {
background-color: #F8F7F9;;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
padding-top:10px;
padding-bottom:20px;
}
.button {
padding: 15px 50px;
font-size: 24px;
text-align: center;
outline: none;
color: #fff;
background-color: #0f8b8d;
border: none;
border-radius: 5px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
/*.button:hover {background-color: #0f8b8d}*/
.button:active {
background-color: #0f8b8d;
box-shadow: 2 2px #CDCDCD;
transform: translateY(2px);
}
.state {
font-size: 1.5rem;
color:#8c8c8c;
font-weight: bold;
}
</style>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
<div>
<h1>ESP WebSocket Server</h2>
</div>
<div>
<div>
<h2>Output - GPIO 2</h2>
<p>state: <span>%STATE%</span></p>
<p><button>Toggle</button></p>
</div>
</div>
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onLoad);
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage; // <-- add this line
}
function onOpen(event) {
console.log('Connection opened');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
function onLoad(event) {
initWebSocket();
initButton();
}
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
function toggle(){
websocket.send('toggle');
}
</script>
</body>
</html>)rawliteral";
void notifyClients() {
ws.textAll(String(ledState));
}
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
if (strcmp((char*)data, "toggle") == 0) {
ledState = !ledState;
notifyClients();
}
}
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
String processor(const String& var){
Serial.println(var);
if(var == "STATE"){
if (ledState){
return "ON";
}
else{
return "OFF";
}
}
return String();
}
void setup(){
// Serial port for debugging purposes
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
// Print ESP Local IP Address
Serial.println(WiFi.localIP());
initWebSocket();
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
// Start ElegantOTA
AsyncElegantOTA.begin(&server);
// Start server
server.begin();
}
void loop() {
ws.cleanupClients();
digitalWrite(ledPin, ledState);
}
View raw code
This is the same code used in this project, but it contains the needed lines of code to handle ElegantOTA:
#include <AsyncElegantOTA.h>
AsyncElegantOTA.begin(&server);
2. Save your sketch: File > Save and give it a name. For example: Web_Server_LED_OTA_ESP32.
3. Generate a .bin file from your sketch. Go to Sketch > Export Compiled Binary. A new .bin file should be created under the project folder.
4. Now, you need to upload that file using the ElegantOTA page. Go to your ESP IP address followed by /update. Make sure you have the firmware option selected. Click on Choose File and select the .bin file you've just generated.
5. When it's finished, click on the Back button.
6. Then, you can go to the root (/) URL to access the new web server. This is the page you should see when you access the ESP IP address on the root (/) URL.
You can click on the button to turn the ESP32 on-board LED on and off.
Because we've also added OTA capabilities to this new web server, we can upload a new sketch in the future if needed. You just need to go to the ESP32 IP address followed by /update.
Congratulations, you've uploaded new code to your ESP32 via Wi-Fi using AsyncElegantOTA.
Continue reading if you want to learn how to upload files to the ESP32 filesystem (SPIFFS) using AsyncElegantOTA.
Upload Files to Filesystem OTA (Over-the-Air) Updates ESP32
In this section you'll learn to upload files to the ESP32 filesystem (SPIFFS) using AsyncElegantOTA.
ESP32 Filesystem Upload Plugin
Before proceeding, you need to have the ESP32 Uploader Plugin installed in your Arduino IDE. Follow the next tutorial before proceeding:
Install ESP32 Filesystem Uploader in Arduino IDE
Web Server with Files from SPIFFS
Imagine the scenario that you need to upload files to the ESP32 filesystem, for example: configuration files; HTML, CSS and JavaScript files to update the web server page; or any other file that you may want to save in SPIFFS via OTA.
To show you how to do this, we'll create a new web server that serves files from SPIFFS: HTML, CSS and JavaScript files to build a web page that controls the ESP32 GPIOs remotely.
Before proceeding make sure you have the Arduino_JSON library by Arduino version 0.1.0 installed. You can install this library in the Arduino IDE Library Manager. Just go to Sketch > Include Library > Manage Libraries and search for the library name as follows: Arduino_JSON.
Copy the following code to your Arduino IDE.
/*
Rui Santos
Complete project details
- Arduino IDE: https://RandomNerdTutorials.com/esp32-ota-over-the-air-arduino/
- VS Code: https://RandomNerdTutorials.com/esp32-ota-over-the-air-vs-code/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
// Import required libraries
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>
#include <AsyncElegantOTA.h>
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
// Create a WebSocket object
AsyncWebSocket ws("/ws");
// Set number of outputs
#define NUM_OUTPUTS 4
// Assign each GPIO to an output
int outputGPIOs[NUM_OUTPUTS] = {2, 4, 12, 14};
// Initialize SPIFFS
void initSPIFFS() {
if (!SPIFFS.begin(true)) {
Serial.println("An error has occurred while mounting SPIFFS");
}
Serial.println("SPIFFS mounted successfully");
}
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
String getOutputStates(){
JSONVar myArray;
for (int i =0; i<NUM_OUTPUTS; i++){
myArray["gpios"][i]["output"] = String(outputGPIOs[i]);
myArray["gpios"][i]["state"] = String(digitalRead(outputGPIOs[i]));
}
String jsonString = JSON.stringify(myArray);
return jsonString;
}
void notifyClients(String state) {
ws.textAll(state);
}
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
if (strcmp((char*)data, "states") == 0) {
notifyClients(getOutputStates());
}
else{
int gpio = atoi((char*)data);
digitalWrite(gpio, !digitalRead(gpio));
notifyClients(getOutputStates());
}
}
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,AwsEventType type,
void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
void setup(){
// Serial port for debugging purposes
Serial.begin(115200);
// Set GPIOs as outputs
for (int i =0; i<NUM_OUTPUTS; i++){
pinMode(outputGPIOs[i], OUTPUT);
}
initSPIFFS();
initWiFi();
initWebSocket();
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html",false);
});
server.serveStatic("/", SPIFFS, "/");
// Start ElegantOTA
AsyncElegantOTA.begin(&server);
// Start server
server.begin();
}
void loop() {
ws.cleanupClients();
}
View raw code
Insert your network credentials in the following variables and save the code.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Update Firmware
Create a .bin file from this sketch as shown previously (this sketch includes the needed lines of code to provide OTA capabilities).
Go to the ESP32 IP address followed by /update and upload the new firmware.
Next, we'll see how to upload the files.
Update Filesystem
Under the project folder, create a folder called data and paste the following HTML, CSS, and JavaScript files (click on the links to download the files):
HTML file: index.html
CSS file: style.css
JavaScript file: script.js
Download all files
To find your project folder, you can simply go to Sketch > Show Sketch Folder.
This is where your data folder should be located and how it looks:
After this, with the ESP32 disconnected from your computer (that's the whole purpose of OTA), click on ESP32 Data Sketch Upload.
You'll get an error because there isn't any ESP32 board connected to your computer don't worry.
Scroll up on the debugging window until you find the .spiffs.bin file location. That's that file that you should upload (in our case the file is called Web_Server_OTA_ESP32_Example_2.spiffs.bin.
And this is the path where our file is located:
C:\Users\sarin\AppData\Local\Temp\arduino_build_675367\Web_server_OTA_ESP32_Example_2.spiffs.bin
To access that file on my computer, I need to make hidden files visible (the AppData folder was not visible). Check if that's also your case.
Once you reach the folder path, you want to get the file with .spiffs.bin extension.
To make things easier you can copy that file to your project folder.
Now that we have a .bin file from the data folder, we can upload that file. Go to your ESP32 IP address followed by /update. Make sure you have the Filesystem option selected.
Then, select the file with the .spiffs.bin extension.
After successfully uploading, click the Back button. And go to the root (/) URL again. You should get access to the following web page that controls the ESP32 outputs using Web Socket protocol.
To see the web server working, you can connect 4 LEDs to your ESP32 on GPIOS: 2, 4, 12, and 14. You should be able to control those outputs from the web server.
If you need to update something on your project, you just need to go to your ESP32 IP address followed by /update.
Congratulations! You've successfully uploaded files to the ESP32 filesystem using AsyncElegantOTA.
Wrapping Up
In this tutorial you've learned how to add OTA capabilities to your Async Web Servers using the AsyncElegantOTA library. This library is super simple to use and allows you to upload new firmware or files to SPIFFS effortlessly using a web page. In our opinion, the AsyncElegantOTA library is one of the best options to handle OTA web updates.
Save Data Permanently using Preferences Library
This guide shows how to save data permanently on the ESP32 flash memory using the Preferences.h library. The data held in the flash memory persists across resets or power failures. Using the Preferences.h library is useful to save data like network credentials, API keys, threshold values, or even the last state of a GPIO. You'll learn how to save and read data from flash memory.
In this tutorial, we'll cover the following topics:
Save key:value pairs;
Read a key value;
Example 1: Save key:value pairs;
Example 2: ESP32 Save/Read Network Credentials using the Preferences.h Library;
Example 3: ESP32 Remember Last GPIO State After RESET;
Preferences.h Library
In a previous tutorial, we recommended using the EEPROM library to save data on flash memory. However, the EEPROM library is deprecated in favor of the Preferences.h library. This library is installed automatically when you install the ESP32 boards in your Arduino IDE.
The Preferences.h library is preferably used to store variable values through key:value pairs. Saving data permanently can be important to:
remember the last state of a variable;
save settings;
save how many times an appliance was activated;
or any other data type you need to save permanently.
If, instead of variables, you need to save files on the ESP32, we recommend using the filesystem (SPIFFS) instead. To learn how to save files in the ESP32 filesystem, you can read one of the following tutorials:
Install ESP32 Filesystem Uploader in Arduino IDE
ESP32 with VS Code and PlatformIO: Upload Files to Filesystem (SPIFFS)
Save Data Using Preferences.h Library
The data saved using preferences is structured like this:
namespace {
key:value
}
You can save different keys on the same namespace, for example:
namespace {
key1: value1
key2: value2
}
In a practical example, this configuration could be used to save your network credentials:
credentials {
ssid: "your_ssid"
pass: "your_pass"
}
In the preceding example, credentials is the namespace, and ssid and pass are the keys.
You can also have multiple namespaces with the same key (but each key with its value):
namespace1{
key:value1
}
namespace2{
key:value2
}
When using the Preferences.h library, you should define the data type you want to save. Later, if you want to read that data, you must know the saved data type. In other words, the data type of writing and reading should be the same.
You can save the following data types using Preferences.h: char, Uchar, short, Ushort, int, Uint, long, Ulong, long64, Ulong64, float, double, bool, string and bytes.
For more information, you can access the Preferences.cpp file here.
Preferences.h Library Useful Functions
To use the Preferences.h library to store data, first you need to include it in your sketch:
#include <Preferences.h>
Then, you must initiate an instance of the Preferences library. You can call it preferences, for example:
Preferences preferences;
After this, you can use the following methods to handle data using the Preferences.h library.
Start Preferences
The begin() method opens a storage space with a defined namespace. The false argument means that we'll use it in read/write mode. Use true to open or create the namespace in read-only mode.
preferences.begin("my-app", false);
In this case, the namespace name is my-app. Namespace name is limited to 15 characters.
Clear Preferences
Use clear() to clear all preferences under the opened namespace (it doesn't delete the namespace):
preferences.clear();
Remove Key
Remove a key from the opened namespace:
preferences.remove(key);
Close Preferences
Use the end() method to close the preferences under the opened namespace:
preferences.end();
Put a Key Value (Save a value)
You should use different methods depending on the variable type you want to save.
Char |
putChar(const char* key, int8_t value) |
Unsigned Char |
putUChar(const char* key, int8_t value) |
Short |
putShort(const char* key, int16_t value) |
Unsigned Short |
putUShort(const char* key, uint16_t value) |
Int |
putInt(const char* key, int32_t value) |
Unsigned Int |
putUInt(const char* key, uint32_t value) |
Long |
putLong(const char* key, int32_t value) |
Unsigned Long |
putULong(const char* key, uint32_t value) |
Long64 |
putLong64(const char* key, int64_t value) |
Unsigned Long64 |
putULong64(const char* key, uint64_t value) |
Float |
putFloat(const char* key, const float_t value) |
Double |
putDouble(const char* key, const double_t value) |
Bool |
putBool(const char* key, const bool value) |
String |
putString(const char* key, const String value) |
Bytes |
putBytes(const char* key, const void* value, size_t len) |
Get a Key Value (Read Value)
Similarly, you should use different methods depending on the variable type you want to get.
Char |
getChar(const char* key, const int8_t defaultValue) |
Unsigned Char |
getUChar(const char* key, const uint8_t defaultValue) |
Short |
getShort(const char* key, const int16_t defaultValue |
Unsigned Short |
getUShort(const char* key, const uint16_t defaultValue) |
Int |
getInt(const char* key, const int32_t defaultValue) |
Unsigned Int |
getUInt(const char* key, const uint32_t defaultValue) |
Long |
getLong(const char* key, const int32_t defaultValue) |
Unsigned Long |
getULong(const char* key, const uint32_t defaultValue) |
Long64 |
getLong64(const char* key, const int64_t defaultValue) |
Unsigned Long64 |
gettULong64(const char* key, const uint64_t defaultValue) |
Float |
getFloat(const char* key, const float_t defaultValue) |
Double |
getDouble(const char* key, const double_t defaultValue) |
Bool |
getBool(const char* key, const bool defaultValue) |
String |
getString(const char* key, const String defaultValue) |
String |
getString(const char* key, char* value, const size_t maxLen) |
Bytes |
getBytes(const char* key, void * buf, size_t maxLen) |
Remove a Namespace
In the Arduino implementation of Preferences, there is no method of completely removing a namespace. As a result, over the course of several projects, the ESP32 non-volatile storage (nvs) Preferences partition may become full. To completely erase and reformat the NVS memory used by Preferences, create a sketch that contains:
#include <nvs_flash.h>
void setup() {
nvs_flash_erase(); // erase the NVS partition and...
nvs_flash_init(); // initialize the NVS partition.
while(true);
}
void loop() {
}
You should download a new sketch to your board immediately after running the above, or it will reformat the NVS partition every time it is powered up.
Preferences.h Save key:value Pairs
For a simple example on how to save and get data using Preferences.h, in your Arduino IDE, go to File > Examples > Preferences > StartCounter.
/*
ESP32 startup counter example with Preferences library.
This simple example demonstrates using the Preferences library to store how many times the ESP32 module has booted.
The Preferences library is a wrapper around the Non-volatile storage on ESP32 processor.
created for arduino-esp32 09 Feb 2017 by Martin Sloup (Arcao)
Complete project details at https://RandomNerdTutorials.com/esp32-save-data-permanently-preferences/
*/
#include <Preferences.h>
Preferences preferences;
void setup() {
Serial.begin(115200);
Serial.println();
// Open Preferences with my-app namespace. Each application module, library, etc
// has to use a namespace name to prevent key name collisions. We will open storage in
// RW-mode (second parameter has to be false).
// Note: Namespace name is limited to 15 chars.
preferences.begin("my-app", false);
// Remove all preferences under the opened namespace
//preferences.clear();
// Or remove the counter key only
//preferences.remove("counter");
// Get the counter value, if the key does not exist, return a default value of 0
// Note: Key name is limited to 15 chars.
unsigned int counter = preferences.getUInt("counter", 0);
// Increase counter by 1
counter++;
// Print the counter to Serial Monitor
Serial.printf("Current counter value: %u\n", counter);
// Store the counter to the Preferences
preferences.putUInt("counter", counter);
// Close the Preferences
preferences.end();
// Wait 10 seconds
Serial.println("Restarting in 10 seconds...");
delay(10000);
// Restart ESP
ESP.restart();
}
void loop() {
}
View raw code
This example increases a variable called counter between resets. This illustrates that the ESP32 remembers the value even after a reset.
Upload the previous sketch to your ESP32 board. Open the Serial Monitor at a baud rate of 115200 and press the on-board RST button. You should see the counter variable increasing between resets.
How the Code Works
This example uses the functions we've seen in the previous sections.
First, include the Preferences.h library.
#include <Preferences.h>
Then, create an instance of the library called preferences.
Preferences preferences;
In the setup(), initialize the Serial Monitor at a baud rate of 115200.
Serial.begin(115200);
Create a storage space in the flash memory called my-app in read/write mode. You can give it any other name.
preferences.begin("my-app", false);
Get the value of the counter key saved on preferences. If it doesn't find any value, it returns 0 by default (which happens when this code runs for the first time).
unsigned int counter = preferences.getUInt("counter", 0);
The counter variable is increased one unit every time the ESP runs:
counter++;
Print the value of the counter variable:
Serial.printf("Current counter value: %u\n", counter);
Store the new value on the counter key:
preferences.putUInt("counter", counter);
Close the Preferences.
preferences.end();
Finally, restart the ESP32 board:
ESP.restart();
ESP32 Save/Read Network Credentials using the Preferences.h Library
The Preferences.h library is many times used to save your network credentials permanently on the flash memory. This way, you don't have to hard code the credentials in every sketch that involves connecting the ESP32 to the internet.
In this section, we'll show you two simple sketches that might be useful in your projects:
Save Network Credentials using Preferences.h
Connect to Wi-Fi with Network Credentials Saved on Preferences
To learn more about ESP32 Wi-Fi related functions, read the following article:
ESP32 Useful Wi-Fi Library Functions (Arduino IDE)
Save Network Credentials using Preferences.h
The following sketch saves your network credentials permanently on the ESP32 flash memory using Preferences.h.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-save-data-permanently-preferences/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Preferences.h>
Preferences preferences;
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
void setup() {
Serial.begin(115200);
Serial.println();
preferences.begin("credentials", false);
preferences.putString("ssid", ssid);
preferences.putString("password", password);
Serial.println("Network Credentials Saved using Preferences");
preferences.end();
}
void loop() {
}
View raw code
Don't forget to insert your network credentials in the following variables:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
How the Code Works
Let's take a quick look at the relevant parts of the code for this example.
In the setup(), create a new storage space on the flash memory with the credentials namespace.
preferences.begin("credentials", false);
Then, create a key called ssid that saves your SSID value (ssid variable) use the putString() method.
preferences.putString("ssid", ssid);
Add another key called password to save the password value (password variable):
preferences.putString("password", password);
So, your data is structured in this way:
credentials{
ssid: your_ssid
password: your_password
}
Upload the code to your board and this is what you should get on the Serial Monitor:
In the following example, we'll show you how to read the network credentials from preferences and use them to connect the ESP32 to your network.
Connect to Wi-Fi with Network Credentials Saved on Preferences
The following sketch gets the network credentials' values and connects to your network using those credentials.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-save-data-permanently-preferences/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Preferences.h>
#include "WiFi.h"
Preferences preferences;
String ssid;
String password;
void setup() {
Serial.begin(115200);
Serial.println();
preferences.begin("credentials", false);
ssid = preferences.getString("ssid", "");
password = preferences.getString("password", "");
if (ssid == "" || password == ""){
Serial.println("No values saved for ssid or password");
}
else {
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid.c_str(), password.c_str());
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
}
void loop() {
// put your main code here, to run repeatedly:
}
View raw code
How the Code Works
Let's take a quick look at the relevant parts of the code for this example.
Open the credentials namespace:
preferences.begin("credentials", false);
Get the SSID and password values using the getString() method. You need to use the key name that you used to save the variables, in this case, ssid and password keys:
ssid = preferences.getString("ssid", "");
password = preferences.getString("password", "");
As a second argument to the getString() function, we passed an empty String. This is the returned value in case there aren't ssid or password keys saved on preferences.
If that's the case, we print a message indicating that there aren't any saved values:
if (ssid == "" || password == ""){
Serial.println("No values saved for ssid or password");
}
Otherwise, we connect to Wi-Fi using the SSID and password saved on preferences.
else {
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid.c_str(), password.c_str());
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
Upload this code to your board after the previous one (to ensure that you have the credentials saved). If everything goes as expected, this is what you should get on your Serial Monitor.
Remember Last GPIO State After RESET
Another application of the Preferences.h library is to save the last state of an output. For example, imagine the following scenario:
You're controlling an output with the ESP32;
You set your output to turn on;
The ESP32 suddenly loses power;
When the power comes back on, the output stays off because it didn't keep its last state.
You don't want this to happen. You want the ESP32 to remember what was happening before losing power and return to the last state.
To solve this problem, you can save the lamp's state in the flash memory. Then, you need to add a condition at the beginning of your sketch to check the last lamp state and turn the lamp on or off accordingly.
The following figure shows what we're going to do:
We'll show you an example using an LED and a pushbutton. The pushbutton controls the LED state. The LED keeps its state between resets. This means that if the LED is lit when you remove power, it will be lit when it gets powered again.
Schematic Diagram
Wire a pushbutton and an LED to the ESP32 as shown in the following schematic diagram.
Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
Code
This is a debounce code that changes the LED state every time you press the pushbutton. But there's something special about this code it remembers the last LED state, even after resetting or removing power from the ESP32. This is possible because we save the led state on Preferences whenever it changes.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-save-data-permanently-preferences/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Preferences.h>
Preferences preferences;
const int buttonPin = 4;
const int ledPin = 5;
bool ledState;
bool buttonState;
int lastButtonState = LOW;
unsigned long lastDebounceTime = 0; // the last time the output pin was toggled
unsigned long debounceDelay = 50; // the debounce time; increase if the output flickers
void setup() {
Serial.begin(115200);
//Create a namespace called "gpio"
preferences.begin("gpio", false);
pinMode(buttonPin, INPUT);
pinMode(ledPin, OUTPUT);
// read the last LED state from flash memory
ledState = preferences.getBool("state", false);
Serial.printf("LED state before reset: %d \n", ledState);
// set the LED to the last stored state
digitalWrite(ledPin, ledState);
}
void loop() {
int reading = digitalRead(buttonPin);
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != buttonState) {
buttonState = reading;
if (buttonState == HIGH) {
ledState = !ledState;
}
}
}
lastButtonState = reading;
if (digitalRead(ledPin)!= ledState) {
Serial.println("State changed");
// change the LED state
digitalWrite(ledPin, ledState);
// save the LED state in flash memory
preferences.putBool("state", ledState);
Serial.printf("State saved: %d \n", ledState);
}
}
View raw code
How the Code Works
Let's take a quick look at the relevant parts of code for this example.
In the setup(), start by creating a section in the flash memory to save the GPIO state. In this example, we've called it gpio.
preferences.begin("gpio", false);
Get the GPIO state saved on Preferences on the state key. It is a boolean variable, so use the getBool() function. If there isn't any state key yet (which happens when the ESP32 first runs), return false (the LED will be off).
ledState = preferences.getBool("state", false);
Print the state and set the LED to the right state:
Serial.printf("LED state before reset: %d \n", ledState);
// set the LED to the last stored state
digitalWrite(ledPin, ledState);
Finally, in the loop() update the state key on Preferences whenever there's a change.
// save the LED state in flash memory
preferences.putBool("state", ledState);
Serial.printf("State saved: %d \n", ledState);
Demonstration
Upload the code to your board and wire the circuit. Open the Serial Monitor at a baud rate of 115200 and press the on-board RST button.
Press the pushbutton to change the LED state and then remove power or press the RST button.
When the ESP32 restarts, it will read the last state saved on Preferences and set the LED to that state. It also prints a message on the Serial Monitor whenever there's a change on the GPIO state.
Wrapping Up
In this tutorial, you've learned how to save data permanently on the ESP32 flash memory using the Preferences.h library. This library is handy to save key:value pairs. Data held on the flash memory remains there even after resetting the ESP32 or removing power.
If you need to store bigger amounts of data or files, you should use the ESP32 filesystem (SPIFFS) or a microSD card instead.
Web Server Hosting Files from MicroSD Card
In this tutorial, you'll learn how to build an ESP32 web server to serve files from a microSD card using a microSD card module that communicates using SPI protocol. You'll learn how to serve your HTML, CSS, JavaScript files, images, and other assets saved on the microSD card. This can be especially useful if your files are too big to fit on the filesystem (SPIFFS), or it can also be more convenient depending on your project. To build the web server, we'll use the ESPAsyncWebServer library.
If you want to build a web server with files from SPIFFS, follow the next tutorial instead:
ESP32 Web Server using SPIFFS (SPI Flash File System)
Project Overview
To show you how to build a web server with files from a microSD card, we'll build a simple HTML page that displays some text formatted with CSS. We'll also serve a favicon. The following image shows the web page we'll serve:
The web server is built using the ESPAsyncWebServer library;
The HTML, CSS, and favicon files are saved on the microSD card;
The microSD card communicates with the ESP32 via SPI communication protocol;
When the client makes a request to the ESP32, it serves the files saved on the microSD card;
You can apply what you'll learn here to any web server project that you are working on.
MicroSD Card Module
There are different microSD card modules compatible with the ESP32. We're using the microSD card module sown in the following figure it communicates using SPI communication protocol. You can use any other microSD card module with an SPI interface.
Where to Buy?
You can click the link below to check different stores where you can get the microSD card module:
MicroSD card module
MicroSD Card Module Pinout
The microSD card module communicates using SPI communication protocol. We'll use the default ESP32 SPI pins as shown in the following table:
microSD card module |
ESP32 |
3V3 |
3.3V |
CS |
GPIO 5 |
MOSI |
GPIO 23 |
CLK |
GPIO 18 |
MISO |
GPIO 19 |
GND |
GND |
Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
Preparing the MicroSD Card
Before proceeding with the tutorial, make sure you format your microSD card as FAT32. Follow the next instructions to format your microSD card or use a software tool like SD Card Formater (compatible with Windows and Mac OS).
1. Insert the microSD card in your computer. Go to My Computer and right click on the SD card. Select Format as shown in figure below.
2. A new window pops up. Select FAT32, press Start to initialize the formatting process and follow the onscreen instructions.
After formatting the microSD card, you can paste the files used to build the web server there. We'll move the index.html, style.css, and favicon.png files into there. Follow the next section to get your files.
HTML File
Create a file called index.html with the following content:
<!DOCTYPE HTML><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="icon" type="image/png" href="favicon.png">
<title>ESP Web Server</title>
</head>
<body>
<h1>Hello World!</h2>
<p>This page was built with files from a microSD card.</p>
</body>
</html>
View raw code
CSS File
Create a file called style.css with the following content:
html {
font-family: Arial;
text-align: center;
}
body {
max-width: 400px;
margin:0px auto;
}
View raw code
Favicon
We'll also serve a favicon. It is a png image with 1515 pixels. You can use the same favicon as ours, or your own icon, or none favicon at all.
You can click on the following link to download the favicon:
Download favicon.png file
Copy Files to MicroSD Card
After having all files prepared, open the microSD card directory and paste the files.
Parts Required
For this tutorial you need the following parts:
ESP32 (read Best ESP32 development boards)
MicroSD card module
MicroSD card
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic Diagram
Wire the microSD card module to the ESP32 as shown in the following schematic diagram. We're using the default ESP32 SPI pins.
Code
Copy the following code to your Arduino IDE.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-web-server-microsd-card/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
void initSDCard(){
if(!SD.begin()){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
}
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
initWiFi();
initSDCard();
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SD, "/index.html", "text/html");
});
server.serveStatic("/", SD, "/");
server.begin();
}
void loop() {
}
View raw code
Insert your network credentials in the following variables and the code should work straight away:
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
How the Code Works
We've covered how to build a web server with the ESP32 in previous tutorials. So, we'll just take a look at the relevant parts for this tutorial.
Libraries
First, make sure you include the FS.h, SD.h and SPI.h libraries to be able to communicate with the microSD card and handle files.
#include "FS.h"
#include "SD.h"
#include "SPI.h"
Initialize MicroSD Card
The initSDCard() function initializes the microSD card on the default SPI pins.
void initSDCard(){
if(!SD.begin()){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
}
Then, you need to call this function in the setup():
initSDCard();
Serve Files From microSD Card
When you access the ESP32 IP address on the root (/) URL, send the HTML file saved on the microSD card.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SD, "/index.html", "text/html");
});
The first argument of the send() function is the filesystem where the files are saved. In this case, it is saved in the SD card (SD). The second argument is the path where the file is located /index.html). The third argument refers to the content type (text/html).
When the HTML file loads on your browser, it will request the CSS and the favicon files. These are static files saved on the same directory (SD). We can add the following line to serve static files in a directory when requested by the root URL. It serves the CSS and favicon files automatically.
server.serveStatic("/", SD, "/");
If your web server needs to handle more routes, you can add them to the setup(). Don't forget to set SD as the first argument to the send() function. This way, it will look for the files in the microSD card.
It's as simple as this. This can be applied to any other web server project.
Demonstration
After uploading the code, open the Serial Monitor at a baud rate of 115200. Press the on-board RST button.
You should get something similar on the Serial Monitor: the ESP32 IP address and information about the microSD card.
Open a browser on your local network and paste the ESP32 IP address. It will load the following web page with the files saved on the microSD card. Notice the favicon on the web browser title bar.
Wrapping Up
In this tutorial, you've learned how to build a web server with the ESP32 with files saved on the microSD card. Instead of writing the HTML, CSS, and JavaScript text directly on the Arduino sketch, you can save them on separate files on a microSD card. This can also be useful to store other data that you might want to display on your web server.
If you don't have a microSD card module, you can save the files on the ESP32 filesystem (SPIFFS) if they fit. You might also like:
ESP32 Data Logging Temperature to MicroSD Card
Useful Wi-Fi Library Functions (Arduino IDE)
This article is a compilation of useful Wi-Fi functions for the ESP32. We'll cover the following topics: scan Wi-Fi networks, connect to a Wi-Fi network, get Wi-Fi connection strength, check connection status, reconnect to the network after a connection is lost, Wi-Fi status, Wi-Fi modes, get the ESP32 IP address, set a fixed IP address and more.
This is not a novelty. There are plenty of examples of how to handle Wi-Fi with the ESP32. However, we thought it would be useful to compile some of the most used and practical Wi-Fi functions for the ESP32.
Table of Contents
Here's a list of what will be covered in this tutorial (you can click on the links to go to the corresponding section):
Wi-Fi Modes: station, access point and both (station + access point);
Scan Wi-Fi networks;
Connect to a Wi-Fi network;
Get Wi-Fi connection status;
Check Wi-Fi connection strength;
Get ESP32 IP address;
Set an ESP32 Static IP address;
Disconnect Wi-Fi;
Reconnect to Wi-Fi after connection is lost;
ESP32 Wi-Fi Events;
Reconnect to Wi-Fi Network After Lost Connection (Wi-Fi Events);
ESP32 WiFiMulti
Change ESP32 Hostname.
Including the Wi-Fi Library
The first thing you need to do to use the ESP32 Wi-Fi functionalities is to include the WiFi.h library in your code, as follows:
#include <WiFi.h>
This library is automatically installed when you install the ESP32 add-on in your Arduino IDE. If you don't have the ESP32 installed, you can follow the next tutorial:
Installing the ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
If you prefer to use VS Code + PaltformIO, you just need to start a new project with an ESP32 board to be able to use the WiFi.h library and its functions.
ESP32 Wi-Fi Modes
The ESP32 board can act as Wi-Fi Station, Access Point or both. To set the Wi-Fi mode, use WiFi.mode() and set the desired mode as argument:
WiFi.mode(WIFI_STA) |
station mode: the ESP32 connects to an access point |
WiFi.mode(WIFI_AP) |
access point mode: stations can connect to the ESP32 |
WiFi.mode(WIFI_AP_STA) |
access point and a station connected to another access point |
Wi-Fi Station
When the ESP32 is set as a Wi-Fi station, it can connect to other networks (like your router). In this scenario, the router assigns a unique IP address to your ESP board. You can communicate with the ESP using other devices (stations) that are also connected to the same network by referring to the ESP unique IP address.
The router is connected to the internet, so we can request information from the internet using the ESP32 board like data from APIs (weather data, for example), publish data to online platforms, use icons and images from the internet or include JavaScript libraries to build web server pages.
Set the ESP32 as a Station and Connect to Wi-Fi Network
Go to Connect to Wi-Fi Network to learn how to set the ESP32 as station and connect it to a network.
In some cases, this might not be the best configuration when you don't have a network nearby and want you still want to connect to the ESP to control it. In this scenario, you must set your ESP board as an access point.
Access Point
When you set your ESP32 board as an access point, you can be connected using any device with Wi-Fi capabilities without connecting to your router. When you set the ESP32 as an access point, you create its own Wi-Fi network, and nearby Wi-Fi devices (stations) can connect to it, like your smartphone or computer. So, you don't need to be connected to a router to control it.
This can be also useful if you want to have several ESP32 devices talking to each other without the need for a router.
Because the ESP32 doesn't connect further to a wired network like your router, it is called soft-AP (soft Access Point). This means that if you try to load libraries or use firmware from the internet, it will not work. It also doesn't work if you make HTTP requests to services on the internet to publish sensor readings to the cloud or use services on the internet (like sending an email, for example).
Set the ESP32 as an Access Point
To set the ESP32 as an access point, set the Wi-Fi mode to access point:
WiFi.mode(WIFI_AP)
And then, use the softAP() method as follows:
WiFi.softAP(ssid, password);
ssid is the name you want to give to the ESP32 access point, and the password variable is the password for the access point. If you don't want to set a password, set it to NULL.
There are also other optional parameters you can pass to the softAP() method. Here are all the parameters:
WiFi.softAP(const char* ssid, const char* password, int channel, int ssid_hidden, int max_connection)
ssid: name for the access point maximum of 63 characters;
password: minimum of 8 characters; set to NULL if you want the access point to be open;
channel: Wi-Fi channel number (1-13)
ssid_hidden: (0 = broadcast SSID, 1 = hide SSID)
max_connection: maximum simultaneous connected clients (1-4)
We have a complete tutorial explaining how to set up the ESP32 as an access point:
How to Set an ESP32 Access Point (AP) for Web Server
Wi-Fi Station + Access Point
The ESP32 can be set as a Wi-Fi station and access point simultaneously. Set its mode to WIFI_AP_STA.
WiFi.mode(WIFI_AP_STA);
Scan Wi-Fi Networks
The ESP32 can scan nearby Wi-Fi networks within its Wi-Fi range. In your Arduino IDE, go to File > Examples > WiFi > WiFiScan. This will load a sketch that scans Wi-Fi networks within the range of your ESP32 board.
This can be useful to check if the Wi-Fi network you're trying to connect is within the range of your board or other applications. Your Wi-Fi project may not often work because it may not be able to connect to your router due to insufficient Wi-Fi strength.
Here's the example:
/*
Example from WiFi > WiFiScan
Complete details at https://RandomNerdTutorials.com/esp32-useful-wi-fi-functions-arduino/
*/
#include "WiFi.h"
void setup() {
Serial.begin(115200);
// Set WiFi to station mode and disconnect from an AP if it was previously connected
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
Serial.println("Setup done");
}
void loop() {
Serial.println("scan start");
// WiFi.scanNetworks will return the number of networks found
int n = WiFi.scanNetworks();
Serial.println("scan done");
if (n == 0) {
Serial.println("no networks found");
} else {
Serial.print(n);
Serial.println(" networks found");
for (int i = 0; i < n; ++i) {
// Print SSID and RSSI for each network found
Serial.print(i + 1);
Serial.print(": ");
Serial.print(WiFi.SSID(i));
Serial.print(" (");
Serial.print(WiFi.RSSI(i));
Serial.print(")");
Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN)?" ":"*");
delay(10);
}
}
Serial.println("");
// Wait a bit before scanning again
delay(5000);
}
View raw code
You can upload it to your board and check the available networks as well as the RSSI (received signal strength indicator).
WiFi.scanNetworks() returns the number of networks found.
int n = WiFi.scanNetworks();
After the scanning, you can access the parameters about each network.
WiFi.SSID() prints the SSID for a specific network:
Serial.print(WiFi.SSID(i));
WiFi.RSSI() returns the RSSI of that network. RSSI stands for Received Signal Strength Indicator. It is an estimated measure of power level that an RF client device is receiving from an access point or router.
Serial.print(WiFi.RSSI(i));
Finally, WiFi.encryptionType() returns the network encryption type. That specific example puts a * in the case of open networks. However, that function can return one of the following options (not just open networks):
WIFI_AUTH_OPEN
WIFI_AUTH_WEP
WIFI_AUTH_WPA_PSK
WIFI_AUTH_WPA2_PSK
WIFI_AUTH_WPA_WPA2_PSK
WIFI_AUTH_WPA2_ENTERPRISE
Connect to a Wi-Fi Network
To connect the ESP32 to a specific Wi-Fi network, you must know its SSID and password. Additionally, that network must be within the ESP32 Wi-Fi range (to check that, you can use the previous example to scan Wi-Fi networks).
You can use the following function to connect the ESP32 to a Wi-Fi network initWiFi():
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
The ssid and password variables hold the SSID and password of the network you want to connect to.
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Then, you simply need to call the initWiFi() function in your setup().
How it Works?
Let's take a quick look on how this function works.
First, set the Wi-Fi mode. If the ESP32 will connected to another network (access point/hotspot) it must be in station mode.
WiFi.mode(WIFI_STA);
Then, use WiFi.begin() to connect to a network. You must pass as arguments the network SSID and its password:
WiFi.begin(ssid, password);
Connecting to a Wi-Fi network can take a while, so we usually add a while loop that keeps checking if the connection was already established by using WiFi.status(). When the connection is successfully established, it returns WL_CONNECTED.
while (WiFi.status() != WL_CONNECTED) {
Get Wi-Fi Connection Status
To get the status of the Wi-Fi connection, you can use WiFi.status(). This returns one of the following values that correspond to the constants on the table:
Value |
Constant |
Meaning |
0 |
WL_IDLE_STATUS |
temporary status assigned when WiFi.begin() is called |
1 |
WL_NO_SSID_AVAIL |
when no SSID are available |
2 |
WL_SCAN_COMPLETED |
scan networks is completed |
3 |
WL_CONNECTED |
when connected to a WiFi network |
4 |
WL_CONNECT_FAILED |
when the connection fails for all the attempts |
5 |
WL_CONNECTION_LOST |
when the connection is lost |
6 |
WL_DISCONNECTED |
when disconnected from a network |
Get WiFi Connection Strength
To get the WiFi connection strength, you can simply call WiFi.RSSI() after a WiFi connection.
Here's an example:
/*
Complete details at https://RandomNerdTutorials.com/esp32-useful-wi-fi-functions-arduino/
*/
#include <WiFi.h>
// Replace with your network credentials (STATION)
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
initWiFi();
Serial.print("RRSI: ");
Serial.println(WiFi.RSSI());
}
void loop() {
// put your main code here, to run repeatedly:
}
View raw code
Insert your network credentials and upload the code.
Open the Serial Monitor and press the ESP32 on-board RST button. It will connect to your network and print the RSSI (received signal strength indicator).
A lower absolute value means a strongest Wi-Fi connection.
Get ESP32 IP Address
When the ESP32 is set as a Wi-Fi station, it can connect to other networks (like your router). In this scenario, the router assigns a unique IP address to your ESP32 board. To get your board's IP address, you need to call WiFi.localIP() after establishing a connection with your network.
Serial.println(WiFi.localIP());
Set a Static ESP32 IP Address
Instead of getting a randomly assigned IP address, you can set an available IP address of your preference to the ESP32 using WiFi.config().
Outside the setup() and loop() functions, define the following variables with your own static IP address and corresponding gateway IP address. By default, the following code assigns the IP address 192.168.1.184 that works in the gateway 192.168.1.1.
// Set your Static IP address
IPAddress local_IP(192, 168, 1, 184);
// Set your Gateway IP address
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);
IPAddress primaryDNS(8, 8, 8, 8); // optional
IPAddress secondaryDNS(8, 8, 4, 4); // optional
Then, in the setup() you need to call the WiFi.config() method to assign the configurations to your ESP32.
// Configures static IP address
if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
Serial.println("STA Failed to configure");
}
The primaryDNS and secondaryDNS parameters are optional and you can remove them.
We recommend reading the following tutorial to learn how to set a static IP address:
ESP32 Static/Fixed IP Address
Disconnect from Wi-Fi Network
To disconnect from a previously connected Wi-Fi network, use WiFi.disconnect():
WiFi.disconnect()
Reconnect to Wi-Fi Network After Lost Connection
To reconnect to Wi-Fi after a connection is lost, you can use WiFi.reconnect() to try to reconnect to the previously connected access point:
WiFi.reconnect()
Or, you can call WiFi.disconnect() followed by WiFi.begin(ssid,password).
WiFi.disconnect();
WiFi.begin(ssid, password);
Alternatively, you can also try to restart the ESP32 with ESP.restart() when the connection is lost.
You can add something like the snippet below to your loop() that checks once in a while if the board is connected.
unsigned long currentMillis = millis();
// if WiFi is down, try reconnecting
if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval)) {
Serial.print(millis());
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WiFi.reconnect();
previousMillis = currentMillis;
}
Don't forget to declare the previousMillis and interval variables. The interval corresponds to the period of time between each check in milliseconds (for example 30 seconds):
unsigned long previousMillis = 0;
unsigned long interval = 30000;
Here's a complete example.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/solved-reconnect-esp32-to-wifi/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
// Replace with your network credentials (STATION)
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
unsigned long previousMillis = 0;
unsigned long interval = 30000;
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
initWiFi();
Serial.print("RSSI: ");
Serial.println(WiFi.RSSI());
}
void loop() {
unsigned long currentMillis = millis();
// if WiFi is down, try reconnecting every CHECK_WIFI_TIME seconds
if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval)) {
Serial.print(millis());
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WiFi.reconnect();
previousMillis = currentMillis;
}
}
View raw code
This example shows how to connect to a network and checks every 30 seconds if it is still connected. If it isn't, it disconnects and tries to reconnect again.
You can read our guide: [SOLVED] Reconnect ESP32 to Wi-Fi Network After Lost Connection.
Alternatively, you can also use WiFi Events to detect that the connection was lost and call a function to handle what to do when that happens (see the next section).
ESP32 Wi-Fi Events
The ESP32 can handle all the following Wi-Fi events (check the source code):
0 |
ARDUINO_EVENT_WIFI_READY |
ESP32 Wi-Fi ready |
1 |
ARDUINO_EVENT_WIFI_SCAN_DONE |
ESP32 finishes scanning AP |
2 |
ARDUINO_EVENT_WIFI_STA_START |
ESP32 station start |
3 |
ARDUINO_EVENT_WIFI_STA_STOP |
ESP32 station stop |
4 |
ARDUINO_EVENT_WIFI_STA_CONNECTED |
ESP32 station connected to AP |
5 |
ARDUINO_EVENT_WIFI_STA_DISCONNECTED |
ESP32 station disconnected from AP |
6 |
ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE |
the auth mode of AP connected by ESP32 station changed |
7 |
ARDUINO_EVENT_WIFI_STA_GOT_IP |
ESP32 station got IP from connected AP |
8 |
ARDUINO_EVENT_WIFI_STA_LOST_IP |
ESP32 station lost IP and the IP is reset to 0 |
9 |
ARDUINO_EVENT_WPS_ER_SUCCESS |
ESP32 station wps succeeds in enrollee mode |
10 |
ARDUINO_EVENT_WPS_ER_FAILED |
ESP32 station wps fails in enrollee mode |
11 |
ARDUINO_EVENT_WPS_ER_TIMEOUT |
ESP32 station wps timeout in enrollee mode |
12 |
ARDUINO_EVENT_WPS_ER_PIN |
ESP32 station wps pin code in enrollee mode |
13 |
ARDUINO_EVENT_WIFI_AP_START |
ESP32 soft-AP start |
14 |
ARDUINO_EVENT_WIFI_AP_STOP |
ESP32 soft-AP stop |
15 |
ARDUINO_EVENT_WIFI_AP_STACONNECTED |
a station connected to ESP32 soft-AP |
16 |
ARDUINO_EVENT_WIFI_AP_STADISCONNECTED |
a station disconnected from ESP32 soft-AP |
17 |
ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED |
ESP32 soft-AP assign an IP to a connected station |
18 |
ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED |
Receive probe request packet in soft-AP interface |
19 |
ARDUINO_EVENT_WIFI_AP_GOT_IP6 |
ESP32 access point v6IP addr is preferred |
19 |
ARDUINO_EVENT_WIFI_STA_GOT_IP6 |
ESP32 station v6IP addr is preferred |
19 |
ARDUINO_EVENT_ETH_GOT_IP6 |
Ethernet IPv6 is preferred |
20 |
ARDUINO_EVENT_ETH_START |
ESP32 ethernet start |
21 |
ARDUINO_EVENT_ETH_STOP |
ESP32 ethernet stop |
22 |
ARDUINO_EVENT_ETH_CONNECTED |
ESP32 ethernet phy link up |
23 |
ARDUINO_EVENT_ETH_DISCONNECTED |
ESP32 ethernet phy link down |
24 |
ARDUINO_EVENT_ETH_GOT_IP |
ESP32 ethernet got IP from connected AP |
25 |
ARDUINO_EVENT_MAX |
|
For a complete example on how to use those events, in your Arduino IDE, go to File > Examples > WiFi > WiFiClientEvents.
/* This sketch shows the WiFi event usage - Example from WiFi > WiFiClientEvents
Complete details at https://RandomNerdTutorials.com/esp32-useful-wi-fi-functions-arduino/ */
/*
* WiFi Events
0 ARDUINO_EVENT_WIFI_READY < ESP32 WiFi ready
1 ARDUINO_EVENT_WIFI_SCAN_DONE < ESP32 finish scanning AP
2 ARDUINO_EVENT_WIFI_STA_START < ESP32 station start
3 ARDUINO_EVENT_WIFI_STA_STOP < ESP32 station stop
4 ARDUINO_EVENT_WIFI_STA_CONNECTED < ESP32 station connected to AP
5 ARDUINO_EVENT_WIFI_STA_DISCONNECTED < ESP32 station disconnected from AP
6 ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE < the auth mode of AP connected by ESP32 station changed
7 ARDUINO_EVENT_WIFI_STA_GOT_IP < ESP32 station got IP from connected AP
8 ARDUINO_EVENT_WIFI_STA_LOST_IP < ESP32 station lost IP and the IP is reset to 0
9 ARDUINO_EVENT_WPS_ER_SUCCESS < ESP32 station wps succeeds in enrollee mode
10 ARDUINO_EVENT_WPS_ER_FAILED < ESP32 station wps fails in enrollee mode
11 ARDUINO_EVENT_WPS_ER_TIMEOUT < ESP32 station wps timeout in enrollee mode
12 ARDUINO_EVENT_WPS_ER_PIN < ESP32 station wps pin code in enrollee mode
13 ARDUINO_EVENT_WIFI_AP_START < ESP32 soft-AP start
14 ARDUINO_EVENT_WIFI_AP_STOP < ESP32 soft-AP stop
15 ARDUINO_EVENT_WIFI_AP_STACONNECTED < a station connected to ESP32 soft-AP
16 ARDUINO_EVENT_WIFI_AP_STADISCONNECTED < a station disconnected from ESP32 soft-AP
17 ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED < ESP32 soft-AP assign an IP to a connected station
18 ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED < Receive probe request packet in soft-AP interface
19 ARDUINO_EVENT_WIFI_AP_GOT_IP6 < ESP32 ap interface v6IP addr is preferred
19 ARDUINO_EVENT_WIFI_STA_GOT_IP6 < ESP32 station interface v6IP addr is preferred
20 ARDUINO_EVENT_ETH_START < ESP32 ethernet start
21 ARDUINO_EVENT_ETH_STOP < ESP32 ethernet stop
22 ARDUINO_EVENT_ETH_CONNECTED < ESP32 ethernet phy link up
23 ARDUINO_EVENT_ETH_DISCONNECTED < ESP32 ethernet phy link down
24 ARDUINO_EVENT_ETH_GOT_IP < ESP32 ethernet got IP from connected AP
19 ARDUINO_EVENT_ETH_GOT_IP6 < ESP32 ethernet interface v6IP addr is preferred
25 ARDUINO_EVENT_MAX
*/
#include <WiFi.h>
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
void WiFiEvent(WiFiEvent_t event){
Serial.printf("[WiFi-event] event: %d\n", event);
switch (event) {
case ARDUINO_EVENT_WIFI_READY:
Serial.println("WiFi interface ready");
break;
case ARDUINO_EVENT_WIFI_SCAN_DONE:
Serial.println("Completed scan for access points");
break;
case ARDUINO_EVENT_WIFI_STA_START:
Serial.println("WiFi client started");
break;
case ARDUINO_EVENT_WIFI_STA_STOP:
Serial.println("WiFi clients stopped");
break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
Serial.println("Connected to access point");
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
Serial.println("Disconnected from WiFi access point");
break;
case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE:
Serial.println("Authentication mode of access point has changed");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
Serial.print("Obtained IP address: ");
Serial.println(WiFi.localIP());
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
Serial.println("Lost IP address and IP address is reset to 0");
break;
case ARDUINO_EVENT_WPS_ER_SUCCESS:
Serial.println("WiFi Protected Setup (WPS): succeeded in enrollee mode");
break;
case ARDUINO_EVENT_WPS_ER_FAILED:
Serial.println("WiFi Protected Setup (WPS): failed in enrollee mode");
break;
case ARDUINO_EVENT_WPS_ER_TIMEOUT:
Serial.println("WiFi Protected Setup (WPS): timeout in enrollee mode");
break;
case ARDUINO_EVENT_WPS_ER_PIN:
Serial.println("WiFi Protected Setup (WPS): pin code in enrollee mode");
break;
case ARDUINO_EVENT_WIFI_AP_START:
Serial.println("WiFi access point started");
break;
case ARDUINO_EVENT_WIFI_AP_STOP:
Serial.println("WiFi access point stopped");
break;
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
Serial.println("Client connected");
break;
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
Serial.println("Client disconnected");
break;
case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
Serial.println("Assigned IP address to client");
break;
case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED:
Serial.println("Received probe request");
break;
case ARDUINO_EVENT_WIFI_AP_GOT_IP6:
Serial.println("AP IPv6 is preferred");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
Serial.println("STA IPv6 is preferred");
break;
case ARDUINO_EVENT_ETH_GOT_IP6:
Serial.println("Ethernet IPv6 is preferred");
break;
case ARDUINO_EVENT_ETH_START:
Serial.println("Ethernet started");
break;
case ARDUINO_EVENT_ETH_STOP:
Serial.println("Ethernet stopped");
break;
case ARDUINO_EVENT_ETH_CONNECTED:
Serial.println("Ethernet connected");
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
Serial.println("Ethernet disconnected");
break;
case ARDUINO_EVENT_ETH_GOT_IP:
Serial.println("Obtained IP address");
break;
default: break;
}}
void WiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(IPAddress(info.got_ip.ip_info.ip.addr));
}
void setup(){
Serial.begin(115200);
// delete old config
WiFi.disconnect(true);
delay(1000);
// Examples of different ways to register wifi events
WiFi.onEvent(WiFiEvent);
WiFi.onEvent(WiFiGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
WiFiEventId_t eventID = WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){
Serial.print("WiFi lost connection. Reason: ");
Serial.println(info.wifi_sta_disconnected.reason);
}, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
// Remove WiFi event
Serial.print("WiFi Event ID: ");
Serial.println(eventID);
// WiFi.removeEvent(eventID);
WiFi.begin(ssid, password);
Serial.println();
Serial.println();
Serial.println("Wait for WiFi... ");
}
void loop(){
delay(1000);
}
View raw code
With Wi-Fi Events, you don't need to be constantly checking the Wi-Fi state. When a certain event happens, it automatically calls the corresponding handling function.
Reconnect to Wi-Fi Network After Lost Connection (Wi-Fi Events)
Wi-Fi events can be useful to detect that a connection was lost and try to reconnect right after (use the SYSTEM_EVENT_AP_STADISCONNECTED event). Here's a sample code:
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/solved-reconnect-esp32-to-wifi/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
void WiFiStationConnected(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("Connected to AP successfully!");
}
void WiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("Disconnected from WiFi access point");
Serial.print("WiFi lost connection. Reason: ");
Serial.println(info.wifi_sta_disconnected.reason);
Serial.println("Trying to Reconnect");
WiFi.begin(ssid, password);
}
void setup(){
Serial.begin(115200);
// delete old config
WiFi.disconnect(true);
delay(1000);
WiFi.onEvent(WiFiStationConnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);
WiFi.onEvent(WiFiGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
WiFi.onEvent(WiFiStationDisconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
/* Remove WiFi event
Serial.print("WiFi Event ID: ");
Serial.println(eventID);
WiFi.removeEvent(eventID);*/
WiFi.begin(ssid, password);
Serial.println();
Serial.println();
Serial.println("Wait for WiFi... ");
}
void loop(){
delay(1000);
}
View raw code
How it Works?
In this example we've added three Wi-Fi events: when the ESP32 connects, when it gets an IP address, and when it disconnects: ARDUINO_EVENT_WIFI_STA_CONNECTED, ARDUINO_EVENT_WIFI_STA_GOT_IP, ARDUINO_EVENT_WIFI_STA_DISCONNECTED.
When the ESP32 station connects to the access point (ARDUINO_EVENT_WIFI_STA_CONNECTED event), the WiFiStationConnected() function will be called:
WiFi.onEvent(WiFiStationConnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);
The WiFiStationConnected() function simply prints that the ESP32 connected to an access point (for example, your router) successfully. However, you can modify the function to do any other task (like light up an LED to indicate that it is successfully connected to the network).
void WiFiStationConnected(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("Connected to AP successfully!");
}
When the ESP32 gets its IP address, the WiFiGotIP() function runs.
WiFi.onEvent(WiFiGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
That function simply prints the IP address on the Serial Monitor.
void WiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
When the ESP32 loses the connection with the access point (ARDUINO_EVENT_WIFI_STA_DISCONNECTED), the WiFiStationDisconnected() function is called.
WiFi.onEvent(WiFiStationDisconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
That function prints a message indicating that the connection was lost and tries to reconnect:
void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("Disconnected from WiFi access point");
Serial.print("WiFi lost connection. Reason: ");
Serial.println(info.wifi_sta_disconnected.reason);
Serial.println("Trying to Reconnect");
WiFi.begin(ssid, password);
}
ESP32 WiFiMulti
The ESP32 WiFiMulti allows you to register multiple networks (SSID/password combinations). The ESP32 will connect to the Wi-Fi network with the strongest signal (RSSI). If the connection is lost, it will connect to the next network on the list. This requires that you include the WiFiMulti.h library (you don't need to install it, it comes by default with the ESP32 package).
To learn how to use WiFiMulti, read the following tutorial:
ESP32 WiFiMulti: Connect to the Strongest Wi-Fi Network (from a list of networks)
Change ESP32 Hostname
To set a custom hostname for your board, call WiFi.setHostname(YOUR_NEW_HOSTNAME); before WiFi.begin();
The default ESP32 hostname is espressif.
There is a method provided by the WiFi.h library that allows you to set a custom hostname.
First, start by defining your new hostname. For example:
String hostname = "ESP32 Node Temperature";
Then, call the WiFi.setHostname() function before calling WiFi.begin(). You also need to call WiFi.config() as shown below:
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
WiFi.setHostname(hostname.c_str()); //define hostname
You can copy the complete example below:
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-set-custom-hostname-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
// Replace with your network credentials (STATION)
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
String hostname = "ESP32 Node Temperature";
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
WiFi.setHostname(hostname.c_str()); //define hostname
//wifi_station_set_hostname( hostname.c_str() );
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
initWiFi();
Serial.print("RRSI: ");
Serial.println(WiFi.RSSI());
}
void loop() {
// put your main code here, to run repeatedly:
}
View raw code
You can use this previous snippet of code in your projects to set a custom hostname for the ESP32.
Important: you may need to restart your router for the changes to take effect.
After this, if you go to your router settings, you'll see the ESP32 with the custom hostname.
Wrapping Up
This article was a compilation of some of the most used and useful ESP32 Wi-Fi functions. Although there are plenty of examples of using the ESP32 Wi-Fi capabilities, there is little documentation explaining how to use the Wi-Fi functions with the ESP32 using Arduino IDE. So, we've decided to put together this guide to make it easier to use ESP32 Wi-Fi-related functions in your projects.
If you have other suggestions, you can share them in the comments section.
Reconnect ESP32 to Wi-Fi Network After Lost Connection
This quick guide shows how you can reconnect your ESP32 to a Wi-Fi network after losing the connection. This can be useful in the following scenarios: the ESP32 temporarily loses Wi-Fi signal; the ESP32 is temporarily out of the router's Wi-Fi range; the router restarts; the router loses internet connection or other situations.
We have a similar guide for the ESP8266 NodeMCU board:
[SOLVED] Reconnect ESP8266 NodeMCU to Wi-Fi Network After Lost Connection
You may also want to take a look at WiFiMulti. It allows you to register multiple networks (SSID/password combinations). The ESP32 will connect to the Wi-Fi network with the strongest signal (RSSI). If the connection is lost, it will connect to the next network on the list. Read: ESP32 WiFiMulti: Connect to the Strongest Wi-Fi Network (from a list of networks).
Reconnect to Wi-Fi Network After Lost Connection
To reconnect to Wi-Fi after a connection is lost, you can use WiFi.reconnect() to try to reconnect to the previously connected access point:
WiFi.reconnect()
Or, you can call WiFi.disconnect() followed by WiFi.begin(ssid,password).
WiFi.disconnect();
WiFi.begin(ssid, password);
Alternatively, you can also try to restart the ESP32 with ESP.restart() when the connection is lost.
You can add something like the snippet below to the loop() that checks once in a while if the board is connected and tries to reconnect if it has lost connection.
unsigned long currentMillis = millis();
// if WiFi is down, try reconnecting
if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval)) {
Serial.print(millis());
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WiFi.reconnect();
previousMillis = currentMillis;
}
Don't forget to declare the previousMillis and interval variables. The interval corresponds to the period of time between each check in milliseconds (for example 30 seconds):
unsigned long previousMillis = 0;
unsigned long interval = 30000;
Here's a complete example.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/solved-reconnect-esp32-to-wifi/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
// Replace with your network credentials (STATION)
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
unsigned long previousMillis = 0;
unsigned long interval = 30000;
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
initWiFi();
Serial.print("RSSI: ");
Serial.println(WiFi.RSSI());
}
void loop() {
unsigned long currentMillis = millis();
// if WiFi is down, try reconnecting every CHECK_WIFI_TIME seconds
if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval)) {
Serial.print(millis());
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WiFi.reconnect();
previousMillis = currentMillis;
}
}
View raw code
This example shows how to connect to a network and checks every 30 seconds if it is still connected. If it isn't, it disconnects and tries to reconnect again.
Alternatively, you can also use Wi-Fi Events to detect that the connection was lost and call a function to handle what to do when that happens (see the next section).
ESP32 Wi-Fi Events
The ESP32 is able to handle different Wi-Fi events. With Wi-Fi Events, you don't need to be constantly checking the Wi-Fi state. When a certain event happens, it automatically calls the corresponding handling function.
The following events are very useful to detect if the connection was lost or reestablished:
ARDUINO_EVENT_WIFI_STA_CONNECTED: the ESP32 is connected in station mode to an access point/hotspot (your router);
ARDUINO_EVENT_WIFI_STA_DISCONNECTED: the ESP32 station disconnected from the access point.
Go to the next section to see an application example.
Reconnect to Wi-Fi Network After Lost Connection (Wi-Fi Events)
Wi-Fi events can be useful to detect that a connection was lost and try to reconnect right after (use the ARDUINO_EVENT_WIFI_STA_DISCONNECTED event). Here's a sample code:
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/solved-reconnect-esp32-to-wifi/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
void WiFiStationConnected(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("Connected to AP successfully!");
}
void WiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("Disconnected from WiFi access point");
Serial.print("WiFi lost connection. Reason: ");
Serial.println(info.wifi_sta_disconnected.reason);
Serial.println("Trying to Reconnect");
WiFi.begin(ssid, password);
}
void setup(){
Serial.begin(115200);
// delete old config
WiFi.disconnect(true);
delay(1000);
WiFi.onEvent(WiFiStationConnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);
WiFi.onEvent(WiFiGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
WiFi.onEvent(WiFiStationDisconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
/* Remove WiFi event
Serial.print("WiFi Event ID: ");
Serial.println(eventID);
WiFi.removeEvent(eventID);*/
WiFi.begin(ssid, password);
Serial.println();
Serial.println();
Serial.println("Wait for WiFi... ");
}
void loop(){
delay(1000);
}
View raw code
How it Works?
In this example, we've added three Wi-Fi events: when the ESP32 connects when it gets an IP address, and when it disconnects: ARDUINO_EVENT_WIDI_STA_CONNECTED, ARDUINO_EVENT_WIFI_STA_GOT_IP, and ARDUINO_EVENT_WIFI_STA_DISCONNECTED, respectively.
When the ESP32 station connects to the access point (ARDUINO_EVENT_WIFI_STA_CONNECTED event), the WiFiStationConnected() function will be called:
WiFi.onEvent(WiFiStationConnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);
The WiFiStationConnected() function simply prints that the ESP32 connected to an access point (for example, your router) successfully. However, you can modify the function to do any other task (like light up an LED to indicate that it is successfully connected to the network).
void WiFiStationConnected(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("Connected to AP successfully!");
}
When the ESP32 gets its IP address, the WiFiGotIP() function runs.
WiFi.onEvent(WiFiGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
That function simply prints the IP address on the Serial Monitor.
void WiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
When the ESP32 loses the connection with the access point (ARDUINO_EVENT_WIFI_STA_DISCONNECTED), the WiFiStationDisconnected() function is called.
WiFi.onEvent(WiFiStationDisconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
That function prints a message indicating that the connection was lost and tries to reconnect:
void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info){
Serial.println("Disconnected from WiFi access point");
Serial.print("WiFi lost connection. Reason: ");
Serial.println(info.disconnected.reason);
Serial.println("Trying to Reconnect");
WiFi.begin(ssid, password);
}
Wrapping Up
This quick tutorial shows different ways of how you can reconnect your ESP32 to a Wi-Fi network after the connection is lost.
We recommend that you take a look at the following tutorial to better understand some of the most used ESP32 Wi-Fi functions:
ESP32 Useful Wi-Fi Library Functions (Arduino IDE)
One of the best applications of the ESP32 Wi-Fi capabilities is building web servers. If you want to use the ESP32 or ESP8266 boards to build Web Server projects, you might like our eBook:
Build Web Servers with the ESP32/ESP8266 eBook (2nd edition)
Get Epoch/Unix Time with the ESP32 (Arduino)
This quick guide shows how to get epoch/unix time using the ESP32 board with Arduino IDE. Getting the epoch time can be useful to timestamp your readings, give unique names to files, and other applications. We'll request the current epoch time from an NTP server, so the ESP32 needs to have an Internet connection.
If you want to get date and time in a human readable format, we recommend the following tutorial instead:
ESP32 NTP Client-Server: Get Date and Time (Arduino IDE)
What is epoch time?
The Epoch Time (also know as Unix epoch, Unix time, POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z).
NTP (Network Time Protocol)
NTP stands for Network Time Protocol and it is a networking protocol for clock synchronization between computer systems. In other words, it is used to synchronize computer clock times in a network.
There are NTP servers like pool.ntp.org that anyone can use to request time as a client. In this case, the ESP32 is an NTP Client that requests time from an NTP Server (pool.ntp.org).
ESP32 Get Epoch/Unix Time Function
To get epoch/unix time with the ESP32, you can use the following function getTime():
// Function that gets current epoch time
unsigned long getTime() {
time_t now;
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
//Serial.println("Failed to obtain time");
return(0);
}
time(&now);
return now;
}
This function returns the current epoch time. Continue reading for a complete example.
ESP32 Get Epoch/Unix Time Example
Copy the following code to your Arduino IDE. This code connects to the internet and requests the time from an NTP server (pool.ntp.org).
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/epoch-unix-time-esp32-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
#include "time.h"
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// NTP server to request epoch time
const char* ntpServer = "pool.ntp.org";
// Variable to save current epoch time
unsigned long epochTime;
// Function that gets current epoch time
unsigned long getTime() {
time_t now;
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
//Serial.println("Failed to obtain time");
return(0);
}
time(&now);
return now;
}
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200);
initWiFi();
configTime(0, 0, ntpServer);
}
void loop() {
epochTime = getTime();
Serial.print("Epoch Time: ");
Serial.println(epochTime);
delay(1000);
}
View raw code
Insert your network credentials in the following variables and the code will work straight away:
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
How the Code Works
Let's take a quick look on how the code works.
Libraries
First, you need to include the WiFi library to connect the ESP32 to your local network; and the time library to handle time structures.
#include <WiFi.h>
#include "time.h"
Network Credentials
Insert your network credentials in the following variables so that the ESP32 can connect to your network.
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
NTP Server
We'll request the time from pool.ntp.org, which is a cluster of timeservers that anyone can use to request the time.
const char* ntpServer = "pool.ntp.org";
getTime() function
The getTime() function gets and returns the current epoch time. If it is not able to get the time, it returns 0.
// Function that gets current epoch time
unsigned long getTime() {
time_t now;
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
//Serial.println("Failed to obtain time");
return(0);
}
time(&now);
return now;
}
initWiFi()
The initWiFi() function initializes Wi-Fi and connects the ESP32 to your local network.
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
setup()
In the setup(), initialize the Serial Monitor at a baud rate of 115200.
Serial.begin(115200);
Call the initWiFi() function to initialize Wi-Fi:
initWiFi();
Configure the time using the configTime() function. The first and second arguments correspond to the GMT time offset and daylight saving time. However, because we're interested in getting epoch time, these arguments should be 0. The last argument is the NTP server (ntpServer):
configTime(0, 0, ntpServer);
loop()
In the loop(), get the epoch time and save it in the epochTime variable:
epochTime = getTime();
Finally, print the epoch time every second:
Serial.print("Epoch Time: ");
Serial.println(epochTime);
delay(1000);
Demonstration
Upload the code to your ESP32 board. Then, open the Serial Monitor at a baud rate of 115200. Press the ESP32 on-board RST button. It should connect to Wi-Fi and start printing the current epoch/unix time every second.
Wrapping Up
This was a quick guide showing you how to get epoch/unix time with the ESP32. The epoch time is the number of seconds elapsed since January 1 1970. To get time, we need to connect to an NTP server, so the ESP32 needs to have access to the internet.
Web Server with MPU-6050 Accelerometer and Gyroscope (3D object representation)
In this project we'll build a web server with the ESP32 to display readings from the MPU-6050 accelerometer and gyroscope sensor. We'll also create a 3D representation of the sensor orientation on the web browser. The readings are updated automatically using Server-Sent Events and the 3D representation is handled using a JavaScript library called three.js. The ESP32 board will be programmed using the Arduino core.
To build the web server we'll use the ESPAsyncWebServer library that provides an easy way to build an asynchronous web server and handle Server-Sent Events.
To learn more about Server-Sent Events, read: ESP32 Web Server using Server-Sent Events (Update Sensor Readings Automatically).
Watch the Video Demonstration
Watch the following video for a preview of the project we'll build.
Project Overview
Before going straight to the project, it's important to outline what our web server will do, so that it is easier to understand.
The web server displays the gyroscope values of the X, Y an Z axis;
The gyroscope values are updated on the web server every 10 milliseconds;
It displays the accelerometer values (X, Y, Z). These values are updated every 200 milliseconds;
The MPU-6050 sensor module also measures temperature, so we'll also display the temperature value. The temperature is updated every second (1000 milliseconds);
All the readings are updated using Server-Sent Events;
There is a 3D representation of the sensor. The orientation of the 3D object changes accordingly to the sensor orientation. The current position of the sensor is calculated using the gyroscope values;
The 3D object is created using a JavaScript library called three.js;
There are four buttons to adjust the position of the 3D object:
RESET POSITION: sets angular position to zero on all axis;
X: sets the X angular position to zero;
Y: sets the Y angular position to zero;
Z: sets the Z angular position to zero;
ESP32 Filesystem
To keep our project organized and make it easier to understand, we'll create four different files to build the web server:
the Arduino code that handles the web server;
HTML file: to define the content of the web page;
CSS file: to style the web page;
JavaScript file: to program the behavior of the web page (handle web server responses, events and creating the 3D object).
The HTML, CSS and JavaScript files will be uploaded to the ESP32 SPIFFS (filesystem). To upload files to the ESP32 filesystem, we'll use the SPIFFS Uploader Plugin.
If you're using PlatformIO + VS Code, read this article to learn how to upload files to the ESP32 filesystem:
ESP32 with VS Code and PlatformIO: Upload Files to Filesystem (SPIFFS)
MPU-6050 Gyroscope and Accelerometer
The MPU-6050 is a module with a 3-axis accelerometer and a 3-axis gyroscope.
The gyroscope measures rotational velocity (rad/s) this is the change of the angular position over time along the X, Y and Z axis (roll, pitch and yaw). This allows us to determine the orientation of an object.
The accelerometer measures acceleration (rate of change of the velocity of an object). It senses static foces like gravity (9.8m/s2) or dynamic forces like vibrations or movement. The MPU-6050 measures acceleration over the X, Y an Z axis. Ideally, in a static object the acceleration over the Z axis is equal to the gravitational force, and it should be zero on the X and Y axis.
Using the values from the accelerometer, it is possible to calculate the roll and pitch angles using trigonometry, but it is not possible to calculate the yaw.
We can combine the information from both sensors to get accurate information about the sensor orientation.
Learn more about the MPU-6050 sensor: ESP32 with MPU-6050 Accelerometer, Gyroscope and Temperature Sensor.
Schematic Diagram ESP32 with MPU-6050
For this project you need the following parts:
MPU-6050 Accelerometer Gyroscope (ESP32 Guide)
ESP32 (read Best ESP32 development boards)
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Wire the ESP32 to the MPU-6050 sensor as shown in the following schematic diagram: connect the SCL pin to GPIO 22 and the SDA pin to GPIO 21.
Preparing Arduino IDE
We'll program the ESP32 board using Arduino IDE. So, make sure you have the ESP32 add-on installed. Follow the next tutorial:
Install the ESP32 Board in Arduino IDE
If you prefer using VSCode + PlatformIO, follow the next tutorial instead:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266
Installing MPU-6050 Libraries
There are different ways to get readings from the sensor. In this tutorial, we'll use the Adafruit MPU6050 library. To use this library you also need to install the Adafruit Unified Sensor library and the Adafruit Bus IO Library.
Open your Arduino IDE and go to Sketch > Include Library > Manage Libraries. The Library Manager should open.
Type adafruit mpu6050 on the search box and install the library.
Then, search for Adafruit Unified Sensor. Scroll all the way down to find the library and install it.
Finally, search for Adafruit Bus IO and install it.
Installing Async Web Server Libraries
To build the web server we'll use the ESPAsyncWebServer library. This library needs the AsyncTCP library to work properly. Click the links below to download the libraries.
ESPAsyncWebServer
AsyncTCP
These libraries aren't available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch > Include Library > Add .zip Library and select the libraries you've just downloaded.
Installing Arduino_JSON library
In this example, we'll send the sensor readings to the browser in JSON format. To make it easier to handle JSON variables, we'll use the Arduino_JSON library.
You can install this library in the Arduino IDE Library Manager. Just go to Sketch > Include Library > Manage Libraries and search for the library name as follows: Arduino_JSON.
If you're using VS Code with PlatformIO, copy the following lines to the platformio.ini file to include all the necessary libraries.
lib_deps = adafruit/Adafruit MPU6050 @ ^2.0.3
adafruit/Adafruit Unified Sensor @ ^1.1.4
me-no-dev/ESP Async WebServer @ ^1.2.3
arduino-libraries/Arduino_JSON @ 0.1.0
Filesystem Uploader Plugin
To follow this tutorial you should have the ESP32 Filesystem Uploader plugin installed in your Arduino IDE. If you don't, follow the next tutorial to install it:
Install ESP32 Filesystem Uploader on Arduino IDE
If you're using VS Code + PlatformIO, follow the next tutorial to learn how to upload files to the ESP32 filesystem:
ESP32 with VS Code and PlatformIO: Upload Files to Filesystem (SPIFFS)
Organizing Your Files
To build the web server you need four different files. The Arduino sketch, the HTML file, the CSS file and the JavaScript file. The HTML, CSS and JavaScript files should be saved inside a folder called data inside the Arduino sketch folder, as shown below:
You can download all project files:
Download ESP_Web_Server_MPU6050.ino, index.html, style.css and script.js
Creating the HTML File
Create an index.html file with the following content or download all the project files.
<!--
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-mpu-6050-web-server/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-->
<!DOCTYPE HTML><html>
<head>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script>
</head>
<body>
<div>
<h1><i></i> MPU6050 <i></i></h2>
</div>
<div>
<div>
<div>
<p>GYROSCOPE</p>
<p><span>X: <span></span> rad</span></p>
<p><span>Y: <span></span> rad</span></p>
<p><span>Z: <span></span> rad</span></p>
</div>
<div>
<p>ACCELEROMETER</p>
<p><span>X: <span></span> ms<sup>2</sup></span></p>
<p><span>Y: <span></span> ms<sup>2</sup></span></p>
<p><span>Z: <span></span> ms<sup>2</sup></span></p>
</div>
<div>
<p>TEMPERATURE</p>
<p><span><span></span> °C</span></p>
<p>3D ANIMATION</p>
<button onclick="resetPosition(this)">RESET POSITION</button>
<button onclick="resetPosition(this)">X</button>
<button onclick="resetPosition(this)">Y</button>
<button onclick="resetPosition(this)">Z</button>
</div>
</div>
<div>
<div></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
View raw code
Head
The <head> and </head> tags mark the start and end of the head. The head is where you insert data about the HTML document that is not directly visible to the end user, but adds functionalities to the web page this is called metadata.
The next line gives a title to the web page. In this case, it is set to ESP Web Server. You can change it if you want. The title is exactly what it sounds like: the title of your document, which shows up in your web browser's title bar.
<title>ESP Web Server</title>
The following meta tag makes your web page responsive. A responsive web design will automatically adjust for different screen sizes and viewports.
<meta name="viewport" content="width=device-width, initial-scale=1">
We use the following meta tag because we won't serve a favicon for our web page in this project.
<link rel="icon" href="data:,">
The styles to style the web page are on a separated file called style.css file. So, we must reference the CSS file on the HTML file as follows.
<link rel="stylesheet" type="text/css" href="style.css">
Include the Font Awesome website styles to include icons in the web page like the gyroscope icon.
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
Finally, we need to include the three.js library to create the 3D representation of the sensor.
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script>
Body
The <body> and </body> tags mark the start and end of the body. Everything that goes inside those tags is the visible page content.
Top Bar
There's a top bar with a heading in the web page. It is a heading 1 and it is placed inside a <div> tag with the class name topnav. Placing your HTML elements between <div> tags, makes them easy to style using CSS.
<div>
<h1><i></i> MPU6050 <i></i></h2>
</div>
Content Grid
All the other content is placed inside a <div> tag called content.
<div>
We use CSS grid layout to display the readings on different aligned boxes (card). Each box corresponds to a grid cell. Grid cells need to be inside a grid container, so the boxes need to be placed inside another <div> tag. This new tag has the classname cards.
<div>
To learn more about CSS grid layout, we recommend this article: A Complete Guide to Grid. Here's the card for the gyroscope readings:
<div>
<p>GYROSCOPE</p>
<p><span>X: <span></span> rad/s</span></p>
<p><span>Y: <span></span> rad/s</span></p>
<p><span>Z: <span></span> rad/s</span></p>
</div>
The card has a title with the name of the card:
<p>GYROSCOPE</p>
And three paragraphs to display the gyroscope values on the X, Y and Z axis.
<p><span>X: <span></span> rad/s</span></p>
<p><span>Y: <span></span> rad/s</span></p>
<p><span>Z: <span></span> rad/s</span></p>
In each paragraph there's a <span> tag with a unique id. This is needed so that we can insert the readings on the right place later using JavaScript. Here's the ids used:
gyroX for the gyroscope X reading;
gyroY for the gyroscope Y reading;
gyroZ for the gyroscope Z reading.
The card to display the accelerometer readings is similar, but with different unique ids for each reading:
<div>
<p>ACCELEROMETER</p>
<p><span>X: <span></span> ms<sup>2</sup></span></p>
<p><span>Y: <span></span> ms<sup>2</sup></span></p>
<p><span>Z: <span></span> ms<sup>2</sup></span></p>
</div>
Here's the ids for the accelerometer readings:
accX for the accelerometer X reading;
accY for the accelerometer Y reading;
accZ for the accelerometer Z reading.
Finally, the following lines display the card for the temperature and the reset buttons.
<div>
<p>TEMPERATURE</p>
<p><span><span></span> °C</span></p>
<p>3D ANIMATION</p>
<button onclick="resetPosition(this)">RESET POSITION</button>
<button onclick="resetPosition(this)">X</button>
<button onclick="resetPosition(this)">Y</button>
<button onclick="resetPosition(this)">Z</button>
</div>
The unique id for the temperature reading is temp.
Then there are four different buttons that when clicked will call the resetPosition() JavaScript function later on. This function will be responsible for sending a request to the ESP32 informing that we want to reset the position, whether its on all the axis or on an individual axis. Each button has a unique id, so that we know which button was clicked:
reset: to reset the position in all axis;
resetX: to reset the position on the X axis;
resetY: to reset the position on the Y axis;
resetZ: to reset the position on the Z axis.
3D Representation
We need to create a section to display the 3D representation.
<div>
<div></div>
</div>
The 3D object will be rendered on the <div> with the 3Dcube id.
Reference the JavaScript File
Finally, because we'll use an external JavaScript file with all the functions to handle the HTML elements and create the 3D animation, we need to reference that file (script.js) as follows:
<script src="script.js"></script>
Creating the CSS File
Create a file called style.css with the following content or download all the project files.
This file is responsible for styling the web page.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-mpu-6050-web-server/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
html {
font-family: Arial;
display: inline-block;
text-align: center;
}
p {
font-size: 1.2rem;
}
body {
margin: 0;
}
.topnav {
overflow: hidden;
background-color: #003366;
color: #FFD43B;
font-size: 1rem;
}
.content {
padding: 20px;
}
.card {
background-color: white;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title {
color:#003366;
font-weight: bold;
}
.cards {
max-width: 800px;
margin: 0 auto;
display: grid; grid-gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.reading {
font-size: 1.2rem;
}
.cube-content{
width: 100%;
background-color: white;
height: 300px; margin: auto;
padding-top:2%;
}
#reset{
border: none;
color: #FEFCFB;
background-color: #003366;
padding: 10px;
text-align: center;
display: inline-block;
font-size: 14px; width: 150px;
border-radius: 4px;
}
#resetX, #resetY, #resetZ{
border: none;
color: #FEFCFB;
background-color: #003366;
padding-top: 10px;
padding-bottom: 10px;
text-align: center;
display: inline-block;
font-size: 14px;
width: 20px;
border-radius: 4px;
}
View raw code
We won't explain how the CSS for this project works because it is not relevant for the goal of this project.
Creating the JavaScript File
Create a file called script.js with the following content or download all the project files.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-mpu-6050-web-server/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
let scene, camera, rendered, cube;
function parentWidth(elem) {
return elem.parentElement.clientWidth;
}
function parentHeight(elem) {
return elem.parentElement.clientHeight;
}
function init3D(){
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
camera = new THREE.PerspectiveCamera(75, parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube")), 0.1, 1000);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(parentWidth(document.getElementById("3Dcube")), parentHeight(document.getElementById("3Dcube")));
document.getElementById('3Dcube').appendChild(renderer.domElement);
// Create a geometry
const geometry = new THREE.BoxGeometry(5, 1, 4);
// Materials of each face
var cubeMaterials = [
new THREE.MeshBasicMaterial({color:0x03045e}),
new THREE.MeshBasicMaterial({color:0x023e8a}),
new THREE.MeshBasicMaterial({color:0x0077b6}),
new THREE.MeshBasicMaterial({color:0x03045e}),
new THREE.MeshBasicMaterial({color:0x023e8a}),
new THREE.MeshBasicMaterial({color:0x0077b6}),
];
const material = new THREE.MeshFaceMaterial(cubeMaterials);
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
renderer.render(scene, camera);
}
// Resize the 3D object when the browser window changes size
function onWindowResize(){
camera.aspect = parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube"));
//camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
//renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setSize(parentWidth(document.getElementById("3Dcube")), parentHeight(document.getElementById("3Dcube")));
}
window.addEventListener('resize', onWindowResize, false);
// Create the 3D representation
init3D();
// Create events for the sensor readings
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('gyro_readings', function(e) {
//console.log("gyro_readings", e.data);
var obj = JSON.parse(e.data);
document.getElementById("gyroX").innerHTML = obj.gyroX;
document.getElementById("gyroY").innerHTML = obj.gyroY;
document.getElementById("gyroZ").innerHTML = obj.gyroZ;
// Change cube rotation after receiving the readinds
cube.rotation.x = obj.gyroY;
cube.rotation.z = obj.gyroX;
cube.rotation.y = obj.gyroZ;
renderer.render(scene, camera);
}, false);
source.addEventListener('temperature_reading', function(e) {
console.log("temperature_reading", e.data);
document.getElementById("temp").innerHTML = e.data;
}, false);
source.addEventListener('accelerometer_readings', function(e) {
console.log("accelerometer_readings", e.data);
var obj = JSON.parse(e.data);
document.getElementById("accX").innerHTML = obj.accX;
document.getElementById("accY").innerHTML = obj.accY;
document.getElementById("accZ").innerHTML = obj.accZ;
}, false);
}
function resetPosition(element){
var xhr = new XMLHttpRequest();
xhr.open("GET", "/"+element.id, true);
console.log(element.id);
xhr.send();
}
View raw code
Creating a 3D Object
The init3D() function creates the 3D object. To actually be able to display anything with three.js, we need three things: scene, camera and renderer, so that we can render the scene with camera.
function init3D(){
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
camera = new THREE.PerspectiveCamera(75, parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube")), 0.1, 1000);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(parentWidth(document.getElementById("3Dcube")), parentHeight(document.getElementById("3Dcube")));
document.getElementById('3Dcube').appendChild(renderer.domElement);
To create the 3D object, we need a BoxGeometry. In the box geometry you can set the dimensions of your object. We created the object with the right proportions to resemble the MPU-6050 shape.
const geometry = new THREE.BoxGeometry(5, 1, 4);
Besides the geometry, we also need a material to color the object. There are different ways to color the object. We've chosen three different colors for the faces.
// Materials of each face
var cubeMaterials = [
new THREE.MeshBasicMaterial({color:0x03045e}),
new THREE.MeshBasicMaterial({color:0x023e8a}),
new THREE.MeshBasicMaterial({color:0x0077b6}),
new THREE.MeshBasicMaterial({color:0x03045e}),
new THREE.MeshBasicMaterial({color:0x023e8a}),
new THREE.MeshBasicMaterial({color:0x0077b6}),
];
const material = new THREE.MeshFaceMaterial(cubeMaterials);
Finally, create the 3D object, add it to the scene and adjust the camera.
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
renderer.render(scene, camera);
We recommend taking a look at this quick three.js tutorial to better understand how it works: Getting Started with three.js Creating a Scene.
To be able to resize the object when the web browser window changes size, we need to call the onWindowResize() function when the event resize occurs.
// Resize the 3D object when the browser window changes size
function onWindowResize(){
camera.aspect = parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube"));
//camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
//renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setSize(parentWidth(document.getElementById("3Dcube")), parentHeight(document.getElementById("3Dcube")));
}
window.addEventListener('resize', onWindowResize, false);
Call the init3D() function to actual create the 3D representation.
init3D();
Events (SSE)
The ESP32 sends new sensor readings periodically as events to the client (browser). We need to handle what happens when the client receives those events.
In this example, we want to place the readings on the corresponding HTML elements and change the 3D object orientation accordingly.
Create a new EventSource object and specify the URL of the page sending the updates. In our case, it /events.
if (!!window.EventSource) {
var source = new EventSource('/events');
Once you've instantiated an event source, you can start listening for messages from the server with addEventListener().
These are the default event listeners, as shown here in the AsyncWebServer documentation.
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
When new gyroscope readings are available, the ESP32 sends an event gyro_readings to the client. We need to add an event listener for that specific event.
source.addEventListener('gyro_readings', function(e) {
The gyroscope readings are a String in JSON format. For example:
{
"gyroX" : "0.09",
"gyroY" : "0.05",
"gyroZ": "0.04"
}
JavaScript has a built-in function to convert a string, written in JSON format, into native JavaScript objects: JSON.parse().
var obj = JSON.parse(e.data);
The obj variable contains the sensor readings in native JavaScript format. Then, we can access the readings as follows:
gyroscope X reading: obj.gyroX;
gyroscope Y reading: obj.gyroY;
gyroscope Z reading: obj.gyroZ;
The following lines put the received data into the corresponding HTML elements on the web page.
document.getElementById("gyroX").innerHTML = obj.gyroX;
document.getElementById("gyroY").innerHTML = obj.gyroY;
document.getElementById("gyroZ").innerHTML = obj.gyroZ;
Finally, we need to change the cube rotation according to the received readings, as follows:
cube.rotation.x = obj.gyroY;
cube.rotation.z = obj.gyroX;
cube.rotation.y = obj.gyroZ;
renderer.render(scene, camera);
Note: in our case, the axis are switched as shown previously (rotation X > gyroY, rotation Z > gyroX, rotation Y > gyroZ). You might need to change this depending on your sensor orientation.
For the accelerometer_readings and temperature events, we simply display the data on the HTML page.
source.addEventListener('temperature_reading', function(e) {
console.log("temperature_reading", e.data);
document.getElementById("temp").innerHTML = e.data;
}, false);
source.addEventListener('accelerometer_readings', function(e) {
console.log("accelerometer_readings", e.data);
var obj = JSON.parse(e.data);
document.getElementById("accX").innerHTML = obj.accX;
document.getElementById("accY").innerHTML = obj.accY;
document.getElementById("accZ").innerHTML = obj.accZ;
}, false);
Finally, we need to create the resetPosition() function. This function will be called by the reset buttons.
function resetPosition(element){
var xhr = new XMLHttpRequest();
xhr.open("GET", "/"+element.id, true);
console.log(element.id);
xhr.send();
}
This function simply sends an HTTP request to the server on a different URL depending on the button that was pressed (element.id).
xhr.open("GET", "/"+element.id, true);
RESET POSITION button > request: /reset
X button > request: /resetX
Y button > request: /resetY
Z button > request: /resetZ
Arduino Sketch
Finally, let's configure the server (ESP32). Copy the following code to the Arduino IDE or download all the project files.
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-mpu-6050-web-server/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Arduino_JSON.h>
#include "SPIFFS.h"
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
// Create an Event Source on /events
AsyncEventSource events("/events");
// Json Variable to Hold Sensor Readings
JSONVar readings;
// Timer variables
unsigned long lastTime = 0;
unsigned long lastTimeTemperature = 0;
unsigned long lastTimeAcc = 0;
unsigned long gyroDelay = 10;
unsigned long temperatureDelay = 1000;
unsigned long accelerometerDelay = 200;
// Create a sensor object
Adafruit_MPU6050 mpu;
sensors_event_t a, g, temp;
float gyroX, gyroY, gyroZ;
float accX, accY, accZ;
float temperature;
//Gyroscope sensor deviation
float gyroXerror = 0.07;
float gyroYerror = 0.03;
float gyroZerror = 0.01;
// Init MPU6050
void initMPU(){
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) {
delay(10);
}
}
Serial.println("MPU6050 Found!");
}
void initSPIFFS() {
if (!SPIFFS.begin()) {
Serial.println("An error has occurred while mounting SPIFFS");
}
Serial.println("SPIFFS mounted successfully");
}
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
Serial.print("Connecting to WiFi...");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
}
Serial.println("");
Serial.println(WiFi.localIP());
}
String getGyroReadings(){
mpu.getEvent(&a, &g, &temp);
float gyroX_temp = g.gyro.x;
if(abs(gyroX_temp) > gyroXerror) {
gyroX += gyroX_temp/50.00;
}
float gyroY_temp = g.gyro.y;
if(abs(gyroY_temp) > gyroYerror) {
gyroY += gyroY_temp/70.00;
}
float gyroZ_temp = g.gyro.z;
if(abs(gyroZ_temp) > gyroZerror) {
gyroZ += gyroZ_temp/90.00;
}
readings["gyroX"] = String(gyroX);
readings["gyroY"] = String(gyroY);
readings["gyroZ"] = String(gyroZ);
String jsonString = JSON.stringify(readings);
return jsonString;
}
String getAccReadings() {
mpu.getEvent(&a, &g, &temp);
// Get current acceleration values
accX = a.acceleration.x;
accY = a.acceleration.y;
accZ = a.acceleration.z;
readings["accX"] = String(accX);
readings["accY"] = String(accY);
readings["accZ"] = String(accZ);
String accString = JSON.stringify (readings);
return accString;
}
String getTemperature(){
mpu.getEvent(&a, &g, &temp);
temperature = temp.temperature;
return String(temperature);
}
void setup() {
Serial.begin(115200);
initWiFi();
initSPIFFS();
initMPU();
// Handle Web Server
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
server.serveStatic("/", SPIFFS, "/");
server.on("/reset", HTTP_GET, [](AsyncWebServerRequest *request){
gyroX=0;
gyroY=0;
gyroZ=0;
request->send(200, "text/plain", "OK");
});
server.on("/resetX", HTTP_GET, [](AsyncWebServerRequest *request){
gyroX=0;
request->send(200, "text/plain", "OK");
});
server.on("/resetY", HTTP_GET, [](AsyncWebServerRequest *request){
gyroY=0;
request->send(200, "text/plain", "OK");
});
server.on("/resetZ", HTTP_GET, [](AsyncWebServerRequest *request){
gyroZ=0;
request->send(200, "text/plain", "OK");
});
// Handle Web Server Events
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
// send event with message "hello!", id current millis
// and set reconnect delay to 1 second
client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);
server.begin();
}
void loop() {
if ((millis() - lastTime) > gyroDelay) {
// Send Events to the Web Server with the Sensor Readings
events.send(getGyroReadings().c_str(),"gyro_readings",millis());
lastTime = millis();
}
if ((millis() - lastTimeAcc) > accelerometerDelay) {
// Send Events to the Web Server with the Sensor Readings
events.send(getAccReadings().c_str(),"accelerometer_readings",millis());
lastTimeAcc = millis();
}
if ((millis() - lastTimeTemperature) > temperatureDelay) {
// Send Events to the Web Server with the Sensor Readings
events.send(getTemperature().c_str(),"temperature_reading",millis());
lastTimeTemperature = millis();
}
}
View raw code
Before uploading the code, make sure you insert your network credentials on the following variables:
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
How the Code Works
Continue reading to learn how the code works or proceed to the next section.
Libraries
First, import all the required libraries for this project:
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Arduino_JSON.h>
#include "SPIFFS.h"
Network Credentials
Insert your network credentials in the following variables:
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
AsyncWebServer and AsyncEventSource
Create an AsyncWebServer object on port 80.
AsyncWebServer server(80);
The following line creates a new event source on /events.
AsyncEventSource events("/events");
Declaring Variables
The readings variable is a JSON variable to hold the sensor readings in JSON format.
JSONVar readings;
In this project, we'll send the gyroscope readings every 10 milliseconds, the accelerometer readings every 200 milliseconds, and the temperature readings every second. So, we need to create auxiliary timer variables for each reading. You can change the delay times if you want.
// Timer variables
unsigned long lastTime = 0;
unsigned long lastTimeTemperature = 0;
unsigned long lastTimeAcc = 0;
unsigned long gyroDelay = 10;
unsigned long temperatureDelay = 1000;
unsigned long accelerometerDelay = 200;
MPU-6050
Create an Adafruit_MPU5060 object called mpu, create events for the sensor readings and variables to hold the readings.
// Create a sensor object
Adafruit_MPU6050 mpu;
sensors_event_t a, g, temp;
float gyroX, gyroY, gyroZ;
float accX, accY, accZ;
float temperature;
Gyroscope Offset
Adjust he gyroscope sensor offset on all axis.
//Gyroscope sensor deviation
float gyroXerror = 0.07;
float gyroYerror = 0.03;
float gyroZerror = 0.01;
To get the sensor offset, go to File > Examples > Adafruit MPU6050 > basic_readings. With the sensor in a static position, check the gyroscope X, Y, and Z values. Then, add those values to the gyroXerror, gyroYerror and gyroZerror variables.
Initialize MPU-6050
The initMPU() function initializes te MPU-6050 sensor.
// Init MPU6050
void initMPU(){
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) {
delay(10);
}
}
Serial.println("MPU6050 Found!");
}
Initialize SPIFFS
The initSPIFFS() function initializes the ESP32 filesystem so that we're able to get access to the files saved on SPIFFS (index.html, style.css and script.js).
void initSPIFFS() {
if (!SPIFFS.begin()) {
Serial.println("An error has occurred while mounting SPIFFS");
}
Serial.println("SPIFFS mounted successfully");
}
Initialize Wi-Fi
The initWiFi() function connects the ESP32 to your local network.
// Initialize WiFi
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
Get Gyroscope Readings
The getGyroReadings() function gets new gyroscope readings and returns the current angular orientation on the X, Y an Z axis as a JSON string.
The gyroscope returns the current angular velocity. The angular velocity is measured in rad/s. To determine the current position of an object, we need to multiply the angular velocity by the elapsed time (10 milliseconds) and add it to the previous position.
current angle (rad) = last angle (rad) + angular velocity (rad/s) * time(s)
The gyroX_temp variable temporarily holds the current gyroscope X value.
float gyroX_temp = g.gyro.x;
To prevent small oscillations of the sensor (see Gyroscope Offset), we first check if the values from the sensor are greater than the offset.
if(abs(gyroX_temp) > gyroXerror) {
If the current value is greater than the offset value, we consider that we have a valid reading. So, we can apply the previous formula to get the current sensor's angular position (gyroX).
gyroX += gyroX_temp / 50.0;
Note: theoretically, we should multiply the current angular velocity by the elapsed time (10 milliseconds = 0.01 seconds (gyroDelay)) or divide by 100. However, after some experiments, we found out that the sensor responds better if we divide by 50.0 instead. Your sensor may be different and you may need to adjust the value.
We follow a similar procedure to get the Y and Z values.
float gyroX_temp = g.gyro.x;
if(abs(gyroX_temp) > gyroXerror) {
gyroX += gyroX_temp/50.00;
}
float gyroY_temp = g.gyro.y;
if(abs(gyroY_temp) > gyroYerror) {
gyroY += gyroY_temp/70.00;
}
float gyroZ_temp = g.gyro.z;
if(abs(gyroZ_temp) > gyroZerror) {
gyroZ += gyroZ_temp/90.00;
}
Finally, we concatenate the readings in a JSON variable (readings) and return a JSON string (jsonString).
readings["gyroX"] = String(gyroX);
readings["gyroY"] = String(gyroY);
readings["gyroZ"] = String(gyroZ);
String jsonString = JSON.stringify(readings);
return jsonString;
Get Accelerometer Readings
The getAccReadings() function returns the accelerometer readings.
String getAccReadings(){
mpu.getEvent(&a, &g, &temp);
// Get current acceleration values
accX = a.acceleration.x;
accY = a.acceleration.y;
accZ = a.acceleration.z;
readings["accX"] = String(accX);
readings["accY"] = String(accY);
readings["accZ"] = String(accZ);
String accString = JSON.stringify (readings);
return accString;
}
Get Temperature Readings
The getTemperature() function returns the current temperature reading.
String getTemperature(){
mpu.getEvent(&a, &g, &temp);
temperature = temp.temperature;
return String(temperature);
}
setup()
In the setup(), initialize the Serial Monitor, Wi-Fi, SPIFFS and the MPU sensor.
void setup() {
Serial.begin(115200);
initWiFi();
initSPIFFS();
initMPU();
Handle Requests
When the ESP32 receives a request on the root URL, we want to send a response with the HTML file (index.html) content that is stored in SPIFFS.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
The first argument of the send() function is the filesystem where the files are saved, in this case it is saved in SPIFFS. The second argument is the path where the file is located. The third argument refers to the content type (HTML text).
In your HTML file, you reference the style.css and script.js files. So, when the HTML file loads on your browser, it will make a request for those CSS and JavaScript files. These are static files saved on the same directory (SPIFFS). So, we can simply add the following line to serve static files in a directory when requested by the root URL. It serves the CSS and JavaScript files automatically.
server.serveStatic("/", SPIFFS, "/");
We also need to handle what happens when the reset buttons are pressed. When you press the RESET POSITION button, the ESP32 receives a request on the /reset path. When that happens, we simply set the gyroX, gyroY and gyroZ variables to zero to restore the sensor initial position.
server.on("/reset", HTTP_GET, [](AsyncWebServerRequest *request){
gyroX=0;
gyroY=0;
gyroZ=0;
request->send(200, "text/plain", "OK");
});
We send an OK response to indicate the request succeeded.
We follow a similar procedure for the other requests (X, Y and Z buttons).
server.on("/resetX", HTTP_GET, [](AsyncWebServerRequest *request){
gyroX=0;
request->send(200, "text/plain", "OK");
});
server.on("/resetY", HTTP_GET, [](AsyncWebServerRequest *request){
gyroY=0;
request->send(200, "text/plain", "OK");
});
server.on("/resetZ", HTTP_GET, [](AsyncWebServerRequest *request){
gyroZ=0;
request->send(200, "text/plain", "OK");
});
The following lines setup the event source on the server.
// Handle Web Server Events
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
// send event with message "hello!", id current millis
// and set reconnect delay to 1 second
client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);
Finally, start the server.
server.begin();
loop() Send Events
In the loop(), we'll send events to the client with the new sensor readings.
The following lines send the gyroscope readings on the gyro_readings event every 10 milliseconds (gyroDelay).
if ((millis() - lastTime) > gyroDelay) {
// Send Events to the Web Server with the Sensor Readings
events.send(getGyroReadings().c_str(),"gyro_readings",millis());
lastTime = millis();
}
Use the send() method on the events object and pass as argument the content you want to send and the name of the event. In this case, we want to send the JSON string returned by the getGyroReadings() function. The send() method accepts a variable of type char, so we need to use the c_str() method to convert the variable. The name of the events is gyro_readings.
We follow a similar procedure for the accelerometer readings, but we use a different event (accelerometer_readings) and a different delay time (accelerometerDelay):
if ((millis() - lastTimeAcc) > accelerometerDelay) {
// Send Events to the Web Server with the Sensor Readings
events.send(getAccReadings().c_str(),"accelerometer_readings",millis());
lastTimeAcc = millis();
}
And finally, for the temperature readings:
if ((millis() - lastTimeTemperature) > temperatureDelay) {
// Send Events to the Web Server with the Sensor Readings
events.send(getTemperature().c_str(),"temperature_reading",millis());
lastTimeTemperature = millis();
}
Uploading Code and Files
After inserting your network credentials, save the code. Go to Sketch > Show Sketch Folder, and create a folder called data.
Inside that folder you should save the HTML, CSS and JavaScript files.
Then, upload the code to your ESP32 board. Make sure you have the right board and COM port selected. Also, make sure you've added your networks credentials to the code.
After uploading the code, you need to upload the files. Go to Tools > ESP32 Data Sketch Upload and wait for the files to be uploaded.
When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the ESP32 EN/RST button, and it should print the ESP32 IP address.
Demonstration
Open your browser and type the ESP32 IP address. You should get access to the web page that shows the sensor readings.
Move the sensor and see the readings changing as well as the 3D object on the browser.
Note: the sensor drifts a bit on the X axis, despite some adjustments in the code. Many of our readers commented that that's normal for this kind of MCUs. To reduce the drifting, some readers suggested using a complementary filter or a kalman filter.
Wrapping Up
The MPU-6050 is an accelerometer, gyroscope and temperature sensor on a single module. In this tutorial you've learned how to build a web server with the ESP32 to display sensor readings from the MPU-6050 sensor. We used server-sent events to send the readings to the client.
Using the three.js JavaScript library we've built a 3D representation of the sensor to show its angular position from the gyroscope readings. The system is not perfect, but it gives an idea of the sensor orientation. If someone more knowledgeable about this topic can share some tips for the sensor calibration it would be greatly appreciated.
with MPU-6050 Accelerometer, Gyroscope and Temperature Sensor (Arduino)
In this guide you'll learn how to use the MPU-6050 accelerometer and gyroscope module with the ESP32. The MPU-6050 IMU (Inertial Measurement Unit) is a 3-axis accelerometer and 3-axis gyroscope sensor. The accelerometer measures the gravitational acceleration and the gyroscope measures the rotational velocity. Additionally, this module also measures temperature. This sensor is ideal to determine the orientation of a moving object.
We have a similar guide for the ESP8266: ESP8266 NodeMCU with MPU-6050 Accelerometer, Gyroscope and Temperature Sensor (Arduino)
In this guide we'll cover two examples:
Get gyroscope, acceleration and temperature readings (Serial Monitor)
Display gyroscope and acceleration readings on OLED display
Introducing the MPU-6050 Gyroscope Accelerometer Sensor
The MPU-6050 is a module with a 3-axis accelerometer and a 3-axis gyroscope.
The gyroscope measures rotational velocity (rad/s), this is the change of the angular position over time along the X, Y and Z axis (roll, pitch and yaw). This allows us to determine the orientation of an object.
The accelerometer measures acceleration (rate of change of the object's velocity). It senses static forces like gravity (9.8m/s2) or dynamic forces like vibrations or movement. The MPU-6050 measures acceleration over the X, Y an Z axis. Ideally, in a static object the acceleration over the Z axis is equal to the gravitational force, and it should be zero on the X and Y axis.
Using the values from the accelerometer, it is possible to calculate the roll and pitch angles using trigonometry. However, it is not possible to calculate the yaw.
We can combine the information from both sensors to get more accurate information about the sensor orientation.
MPU-6050 Pinout
Here's the pinout for the MPU-6050 sensor module.
VCC |
Power the sensor (3.3V or 5V) |
GND |
Common GND |
SCL |
SCL pin for I2C communication (GPIO 22) |
SDA |
SDA pin for I2C communication (GPIO 21) |
XDA |
Used to interface other I2C sensors with the MPU-6050 |
XCL |
Used to interface other I2C sensors with the MPU-6050 |
AD0 |
Use this pin to change the I2C address |
INT |
Interrupt pin can be used to indicate that new measurement data is available |
Preparing Arduino IDE
We'll program the ESP32 board using Arduino IDE. So, make sure you have the ESP32 add-on installed. Follow the next tutorial:
Install the ESP32 Board in Arduino IDE
If you prefer using VS Code + PlatformIO IDE, follow the next guide:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266
Installing Libraries
There are different ways to get readings from the sensor. In this tutorial, we'll use the Adafruit MPU6050 library. To use this library you also need to install the Adafruit Unified Sensor library and the Adafruit Bus IO Library.
Open your Arduino IDE and go to Sketch > Include Library > Manage Libraries. The Library Manager should open.
Type adafruit mpu6050 on the search box and install the library.
Then, search for Adafruit Unified Sensor. Scroll all the way down to find the library and install it.
Finally, search for Adafruit Bus IO and install it.
After installing the libraries, restart your Arduino IDE.
If you're using VS Code with PaltformIO, copy the following lines to the platformio.ini file.
lib_deps = adafruit/Adafruit MPU6050 @ ^2.0.3
adafruit/Adafruit Unified Sensor @ ^1.1.4
Getting MPU-6050 Sensor Readings: Accelerometer, Gyroscope and Temperature
In this section you'll learn how to get sensor readings from the MPU-6050 sensor: acceleration (x, y, z), angular velocity (x, y, z) and temperature.
Parts Required
For this example you need the following parts:
MPU-6050 Accelerometer Gyroscope
ESP32 (read Best ESP32 development boards)
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic Diagram ESP32 with MPU-6050
Wire the ESP32 to the MPU-6050 sensor as shown in the following schematic diagram: connect the SCL pin to GPIO 22 and the SDA pin to GPIO 21.
Code Getting MPU-6050 Sensor Readings: Accelerometer, Gyroscope and Temperature
The Adafruit library provides several examples for this sensor. In this section, we'll take a look at the basic example that prints the sensor readings in the Serial Monitor.
Go to File > Examples > Adafruit MPU6050 > basic_readings. The following code should load.
It gets the angular velocity (gyroscope) on the x, y and z axis, the acceleration on the x, y and z axis and the temperature.
// Basic demo for accelerometer readings from Adafruit MPU6050
// ESP32 Guide: https://RandomNerdTutorials.com/esp32-mpu-6050-accelerometer-gyroscope-arduino/
// ESP8266 Guide: https://RandomNerdTutorials.com/esp8266-nodemcu-mpu-6050-accelerometer-gyroscope-arduino/
// Arduino Guide: https://RandomNerdTutorials.com/arduino-mpu-6050-accelerometer-gyroscope/
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
Adafruit_MPU6050 mpu;
void setup(void) {
Serial.begin(115200);
while (!Serial)
delay(10); // will pause Zero, Leonardo, etc until serial console opens
Serial.println("Adafruit MPU6050 test!");
// Try to initialize!
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) {
delay(10);
}
}
Serial.println("MPU6050 Found!");
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
Serial.print("Accelerometer range set to: ");
switch (mpu.getAccelerometerRange()) {
case MPU6050_RANGE_2_G:
Serial.println("+-2G");
break;
case MPU6050_RANGE_4_G:
Serial.println("+-4G");
break;
case MPU6050_RANGE_8_G:
Serial.println("+-8G");
break;
case MPU6050_RANGE_16_G:
Serial.println("+-16G");
break;
}
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
Serial.print("Gyro range set to: ");
switch (mpu.getGyroRange()) {
case MPU6050_RANGE_250_DEG:
Serial.println("+- 250 deg/s");
break;
case MPU6050_RANGE_500_DEG:
Serial.println("+- 500 deg/s");
break;
case MPU6050_RANGE_1000_DEG:
Serial.println("+- 1000 deg/s");
break;
case MPU6050_RANGE_2000_DEG:
Serial.println("+- 2000 deg/s");
break;
}
mpu.setFilterBandwidth(MPU6050_BAND_5_HZ);
Serial.print("Filter bandwidth set to: ");
switch (mpu.getFilterBandwidth()) {
case MPU6050_BAND_260_HZ:
Serial.println("260 Hz");
break;
case MPU6050_BAND_184_HZ:
Serial.println("184 Hz");
break;
case MPU6050_BAND_94_HZ:
Serial.println("94 Hz");
break;
case MPU6050_BAND_44_HZ:
Serial.println("44 Hz");
break;
case MPU6050_BAND_21_HZ:
Serial.println("21 Hz");
break;
case MPU6050_BAND_10_HZ:
Serial.println("10 Hz");
break;
case MPU6050_BAND_5_HZ:
Serial.println("5 Hz");
break;
}
Serial.println("");
delay(100);
}
void loop() {
/* Get new sensor events with the readings */
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
/* Print out the values */
Serial.print("Acceleration X: ");
Serial.print(a.acceleration.x);
Serial.print(", Y: ");
Serial.print(a.acceleration.y);
Serial.print(", Z: ");
Serial.print(a.acceleration.z);
Serial.println(" m/s^2");
Serial.print("Rotation X: ");
Serial.print(g.gyro.x);
Serial.print(", Y: ");
Serial.print(g.gyro.y);
Serial.print(", Z: ");
Serial.print(g.gyro.z);
Serial.println(" rad/s");
Serial.print("Temperature: ");
Serial.print(temp.temperature);
Serial.println(" degC");
Serial.println("");
delay(500);
}
View raw code
How the Code Works
Start by including the required libraries for the MPU-6050 sensor: Adafruit_MPU6050 and Adafruit_Sensor.
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
Create an Adafruit_MPU6050 object called mpu to handle the sensor.
Adafruit_MPU6050 mpu;
setup()
In the setup(), initialize the serial monitor at a baud rate of 115200.
Serial.begin(115200);
Initialize the MPU-6050 sensor.
if (!mpu.begin()) {
Serial.println("Sensor init failed");
while (1)
yield();
}
Set the accelerometer measurement range:
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
Set the gyroscope measurement range:
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
Set the filter bandwidth:
mpu.setFilterBandwidth(MPU6050_BAND_5_HZ);
loop()
In the loop() we'll get sensor readings and display them in the Serial Monitor.
First, you need to get new sensor events with the current readings.
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
Finally, print the readings. For the acceleration:
a.acceleration.x: gets acceleration on the x axis;
a.acceleration.y: gets acceleration on the y axis;
a.acceleration.z: gets acceleration on the z axis.
The acceleration is measured in meters per second square (m/s2)
Serial.print("Acceleration X: ");
Serial.print(a.acceleration.x);
Serial.print(", Y: ");
Serial.print(a.acceleration.y);
Serial.print(", Z: ");
Serial.print(a.acceleration.z);
Serial.println(" m/s^2");
To get gyroscope readings:
g.gyro.x: gets angular velocity on the x axis;
g.gyro.y: gets angular velocity on the y axis;
g.gyro.z: gets angular velocity on the z axis.
The angular velocity is measured in radians per seconds (rad/s).
Serial.print("Rotation X: ");
Serial.print(g.gyro.x);
Serial.print(", Y: ");
Serial.print(g.gyro.y);
Serial.print(", Z: ");
Serial.print(g.gyro.z);
Serial.println(" rad/s");
Finally, print the temperature it is measured in Celsius degrees. To access the temperature reading use temp.temperature.
Serial.print("Temperature: ");
Serial.print(temp.temperature);
Serial.println(" degC");
New sensor readings are displayed every 500 milliseconds.
delay(500);
Demonstration
Upload the code to your ESP32 board. Go to Tools > Board and select the ESP32 board you're using. Go to Tools > Port and select the port your board is connected to. Then, click the Upload button.
Open the Serial Monitor at a baud rate of 115200, press the on-board RST button. The sensor measurements will be displayed.
Move the sensor orientation and see the values changing accordingly.
Sensor Calibration
Ideally, when the sensor is static, the gyroscope values should be zero on all axis, which doesn't happen in our case. When the sensor is static, these are the gyroscope values we get:
x: 0.06 rad/s
y: -0.02 rad/s
z: 0.00 rad/s
On practical applications, you need to take the error into account and correct the values in the code to get more accurate readings.
The same happens for the acceleration values. The acceleration on the z axis should be closer to the gravitational force (9,8 m/s2) and it should be closer to zero on the x and y axis. In our case, these are the approximate values we get when the sensor is static:
x: 0.71 m/s2
y: 0.28 m/s2
z: 9.43 m/s2
Display MPU-6050 Readings on OLED Display
The Adafruit MPU6050 library provides an example that dipslays the MPU-6050 gyroscope and accelerometer readings on an OLED display.
Parts Required
Here's a list with the parts required to complete this example:
MPU-6050 Accelerometer Gyroscope
ESP32 (read Best ESP32 development boards)
0.96 inch I2C OLED Display SSD1306
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic Diagram ESP32 with MPU-6050 and OLED Display
Wire all the parts as shown in the following schematic diagram. Because the OLED display and the MPU-6050 sensors use different I2C addresses, we can connect them to the same I2C bus (same pins on the ESP32).
Learn more about using the OLED display with the ESP32: ESP32 OLED Display with Arduino IDE
Code Display MPU-6050 Sensor Readings on OLED Display
To use this example, make sure you have the Adafruit SSD1306 library installed. This library can be installed through the Arduino Library Manager.
Go to Sketch > Library > Manage Libraries and search for SSD1306 and install the SSD1306 library from Adafruit.
For this example, copy the following code or go to File > Examples > Adafruit MPU6050 > MPU6050_oled.
// Basic OLED demo for accelerometer readings from Adafruit MPU6050
// ESP32 Guide: https://RandomNerdTutorials.com/esp32-mpu-6050-accelerometer-gyroscope-arduino/
// ESP8266 Guide: https://RandomNerdTutorials.com/esp8266-nodemcu-mpu-6050-accelerometer-gyroscope-arduino/
// Arduino Guide: https://RandomNerdTutorials.com/arduino-mpu-6050-accelerometer-gyroscope/
#include <Adafruit_MPU6050.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
Adafruit_MPU6050 mpu;
Adafruit_SSD1306 display = Adafruit_SSD1306(128, 64, &Wire);
void setup() {
Serial.begin(115200);
// while (!Serial);
Serial.println("MPU6050 OLED demo");
if (!mpu.begin()) {
Serial.println("Sensor init failed");
while (1)
yield();
}
Serial.println("Found a MPU-6050 sensor");
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
Serial.println(F("SSD1306 allocation failed"));
for (;;)
; // Don't proceed, loop forever
}
display.display();
delay(500); // Pause for 2 seconds
display.setTextSize(1);
display.setTextColor(WHITE);
display.setRotation(0);
}
void loop() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
display.clearDisplay();
display.setCursor(0, 0);
Serial.print("Accelerometer ");
Serial.print("X: ");
Serial.print(a.acceleration.x, 1);
Serial.print(" m/s^2, ");
Serial.print("Y: ");
Serial.print(a.acceleration.y, 1);
Serial.print(" m/s^2, ");
Serial.print("Z: ");
Serial.print(a.acceleration.z, 1);
Serial.println(" m/s^2");
display.println("Accelerometer - m/s^2");
display.print(a.acceleration.x, 1);
display.print(", ");
display.print(a.acceleration.y, 1);
display.print(", ");
display.print(a.acceleration.z, 1);
display.println("");
Serial.print("Gyroscope ");
Serial.print("X: ");
Serial.print(g.gyro.x, 1);
Serial.print(" rps, ");
Serial.print("Y: ");
Serial.print(g.gyro.y, 1);
Serial.print(" rps, ");
Serial.print("Z: ");
Serial.print(g.gyro.z, 1);
Serial.println(" rps");
display.println("Gyroscope - rps");
display.print(g.gyro.x, 1);
display.print(", ");
display.print(g.gyro.y, 1);
display.print(", ");
display.print(g.gyro.z, 1);
display.println("");
display.display();
delay(100);
}
View raw code
How the Code Works
Start by including the required libraries for the MPU-6050 sensor and for the OLED display.
#include <Adafruit_MPU6050.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
Create an Adafruit_MPU6050 object called mpu to handle the sensor.
Adafruit_MPU6050 mpu;
Create an Adafruit_SSD1306 object called display to handle the OLED display. This is for a display with 12864 pixels.
Adafruit_SSD1306 display = Adafruit_SSD1306(128, 64, &Wire);
setup()
In the setup(), initialize the serial monitor at a baud rate of 115200.
Serial.begin(115200);
Initialize the MPU-6050 sensor.
if (!mpu.begin()) {
Serial.println("Sensor init failed");
while (1)
yield();
}
Initialize the OLED display.
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
Serial.println(F("SSD1306 allocation failed"));
for (;;)
; // Don't proceed, loop forever
}
display.display();
Set the font size and color for the display.
display.setTextSize(1);
display.setTextColor(WHITE);
display.setRotation(0);
loop()
In the loop() is where we'll get the sensor readings and display them on the OLED.
Start by creating events for each measurement, accelerometer, gyroscope and temperature.
sensors_event_t a, g, temp;
Get new sensor readings.
mpu.getEvent(&a, &g, &temp);
Clear the display in each loop() to write new readings.
display.clearDisplay();
Set the display cursor to (0,0) the upper left corner. It will start writing text from that location.
display.setCursor(0, 0);
The following lines print the accelerometer readings in the Serial Monitor.
Serial.print("Accelerometer ");
Serial.print("X: ");
Serial.print(a.acceleration.x, 1);
Serial.print(" m/s^2, ");
Serial.print("Y: ");
Serial.print(a.acceleration.y, 1);
Serial.print(" m/s^2, ");
Serial.print("Z: ");
Serial.print(a.acceleration.z, 1);
Serial.println(" m/s^2");
The following lines display the acceleration x, y an z values on the OLED display.
display.println("Accelerometer - m/s^2");
display.print(a.acceleration.x, 1);
display.print(", ");
display.print(a.acceleration.y, 1);
display.print(", ");
display.print(a.acceleration.z, 1);
display.println("");
Display the gyroscope readings on the Serial Monitor.
Serial.print("Gyroscope ");
Serial.print("X: ");
Serial.print(g.gyro.x, 1);
Serial.print(" rps, ");
Serial.print("Y: ");
Serial.print(g.gyro.y, 1);
Serial.print(" rps, ");
Serial.print("Z: ");
Serial.print(g.gyro.z, 1);
Serial.println(" rps");
Finally, print the gyroscope readings on the display.
display.println("Gyroscope - rps");
display.print(g.gyro.x, 1);
display.print(", ");
display.print(g.gyro.y, 1);
display.print(", ");
display.print(g.gyro.z, 1);
display.println("");
Lastly, call display.display() to actually show the readings on the OLED.
display.display();
New readings are displayed every 100 milliseconds.
delay(100);
Demonstration
Upload the code to your ESP32 board. Go to Tools > Board and select the ESP32 board you're using. Go to Tools > Port and select the port your board is connected to. Then, click the upload button.
Open the Serial Monitor at a baud rate of 115200, press the on-board RST button. The sensor measurements will be displayed both on the Serial Monitor and on the OLED display.
Move the sensor and see the values changing.
You can watch the video demonstration:
Wrapping Up
The MPU-6050 is an accelerometer and gyroscope. It measures acceleration on the x, y and z axis as well as angular velocity. This module also measures temperature.
This sensor modules communicates via I2C communication protocol. So, the wiring is very simple. Just connect the sensor to the ESP32 default I2C pins.
In this tutorial you've learned how to wire the sensor and get sensor readings. We hope you've found this getting started guide useful.
with VS Code and PlatformIO: Upload Files to Filesystem (SPIFFS)
Learn how to upload files to the ESP32 board filesystem (SPIFFS) using VS Code with the PlatformIO IDE extension (quick and easy). Using the filesystem with the ESP32 can be useful to save HTML, CSS and JavaScript files to build a web server instead of having to write everything inside the Arduino sketch.
If you're using Arduino IDE follow this tutorial instead: Install ESP32 Filesystem Uploader in Arduino IDE.
Introducing SPIFFS
The ESP32 contains a Serial Peripheral Interface Flash File System (SPIFFS). SPIFFS is a lightweight filesystem created for microcontrollers with a flash chip, which are connected by SPI bus, like the ESP32 flash memory.
SPIFFS lets you access the flash memory like you would do in a normal filesystem in your computer, but simpler and more limited. You can read, write, close, and delete files. SPIFFS doesn't support directories, so everything is saved on a flat structure.
Using SPIFFS with the ESP32 board is specially useful to:
Create configuration files with settings;
Save data permanently;
Create files to save small amounts of data instead of using a microSD card;
Save HTML, CSS and JavaScript files to build a web server;
Save images, figures and icons;
And much more.
Upload Files to ESP32 SPIFFS
The files you want to upload to the ESP32 filesystem should be placed in a folder called data under the project folder. For you to understand how everything works, we'll upload a .txt file with some random text. You can upload any other file type.
If you're not familiar with VS Code + PlatformIO, follow the next tutorial first:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266
Creating a data Folder
Create a folder called data inside your project folder. This can be done on VS Code.
With your mouse, select the project folder you're working on. Click on the New Folder icon to create a new folder.
This new folder must be called data, otherwise, it won't work.
Then, select the newly created data folder and create the files you want to upload by clicking on the New File icon. In this example, we'll create a file called text.txt. You can create and upload any other file types like .html, .css or .js files, for example.
Write some random text inside that .txt file.
The data folder should be under the project folder and the files you want to upload should be inside the data folder. Otherwise, it won't work.
Uploading Filesystem Image
After creating and saving the file or files you want to upload under the data folder, follow the next steps:
Click the PIO icon at the left side bar. The project tasks should open.
Select env:esp32doit-devkit-v1 (it may be slightly different depending on the board you're using).
Expand the Platform menu.
Select Build Filesystem Image.
Finally, click Upload Filesystem Image.
Important: to upload the filesystem image successfully you must close all serial
connections (Serial Monitor) with your board.
After a while, you should get a success message.
Troubleshooting
Here's some common mistakes:
Could not open port COMX Access is denied.
This error means that you have a serial connection opened with your board in VS Code or in any other program. Close any program that might be using the board serial port, and make sure you close all serial connections in VS Code (click on the recycle bin icon on the terminal console).
Timed out waiting for packet header error
If you start seeing a lot of dots on the debugging window and the filesystem image fails to upload, you need to press the on-board boot button once you start seeing the dots.
To solve this issue permanently, read the following article:
[SOLVED] Failed to connect to ESP32: Timed out waiting for packet header
Testing
Now, let's just check if the file was actually saved into the ESP32 filesystem. Copy the following code to the main.cpp file and upload it to your board.
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-vs-code-platformio-spiffs/
*********/
#include <Arduino.h>
#include "SPIFFS.h"
void setup() {
Serial.begin(9600);
if(!SPIFFS.begin(true)){
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
File file = SPIFFS.open("/text.txt");
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.println("File Content:");
while(file.available()){
Serial.write(file.read());
}
file.close();
}
void loop() {
}
View raw code
You may need to change the following line depending on the name of your file.
File file = SPIFFS.open("/text.txt");
Open the Serial Monitor and it should print the content of your file.
You've successfully uploaded files to the ESP32 filesystem (SPIFFS) using VS Code + PlatformIO.
Wrapping Up
With this tutorial you've learned how to upload files to the ESP32 filesystem (SPIFFS) using VS Code + PlatformIO. It is quick and easy.
This can be specially useful to upload HTML, CSS and JavaScript files to build web server projects with the ESP32 boards.
Weather Station Interface PCB Shield (Temperature, Humidity, Pressure, Date and Time)
In this project, you'll learn how to build a Weather Station Interface PCB Shield for the ESP32 development board. The PCB features a BME280 temperature, humidity and pressure sensor, a light dependent resistor, a pushbutton, an OLED display and multiple WS2812B addressable RGB LEDs. The OLED displays the sensor readings and the LEDs produce different lighting effects to what is shown in the OLED. It also displays date and time.
Watch the Video Tutorial
This project is available in video format and in written format. You can watch the video below or you can scroll down for the written instructions.
Resources
You can find all the resources needed to build this project in the links below (or you can visit the GitHub project page):
ESP32 Code (Arduino IDE)
Gerber files
EasyEDA project to edit the PCB
Click here to download all the files
Project Overview
Weather Station PCB Hardware Features
The shield is designed with some headers pins to stack the ESP32 board. For this reason, if you want to build and use our PCB, you need to get the same ESP32 development board. We're using the ESP32 DEVKIT DOIT V1 board (the model with 36 GPIOs).
If you want to follow this project and you have a different ESP32 model, you can assemble the circuit on a breadboard or you can modify the PCB layout and wiring to match the pinout of your ESP32 board. Throughout this project, we provide all the necessary files, if you need to modify the PCB.
The shield consists of:
BME280 temperature, humidity and pressure sensor;
LDR (light dependent resistor luminosity sensor);
0.96 inch I2C OLED Display;
Pushbutton;
12 WS2812B addressable RGB LEDs;
If you're going to replicate this project on a breadboard, instead of individual WS2812B addressable RGB LEDs, you can use an addressable RGB LED strip or an addressable RGB LED ring with the same number of LEDs (12).
Weather Station PCB Pin Assignment
The following table shows the pin assignment for each component on the shield:
Component |
ESP32 Pin Assignment |
BME280 |
GPIO 21 (SDA), GPIO 22 (SCL) |
OLED Display |
GPIO 21 (SDA), GPIO 22 (SCL) |
Light Dependent Resistor (LDR) |
GPIO 33 |
Pushbutton |
GPIO 18 |
Addressable RGB LEDs |
GPIO 27 |
Weather Station PCB Software Features
There are endless ways to program the same circuit to get different outcomes with different functionalities and features. In this particular project we'll program the PCB as follows:
The OLED displays five different screens:
Current date and time;
Temperature
Humidity
Pressure
Luminosity
Each screen is shown for 15 seconds before going to the next one.
Alternatively, you can press the pushbutton to change screens.
In each screen the WS2812B addressable RGB LEDs show a different pattern:
On the date and time screen, the RGB LEDs display a rainbow effect;
On the other screens, the RGB LEDs work like a gauge. For example, 100% humidity lights up all LEDs, 50% humidify lights up half of the number of LEDs.
The color of the LEDs is different for each screen: green for the temperature, blue for the humidity, purple for pressure and yellow for luminosity.
Testing the Circuit on a Breadboard
Before designing and building the PCB, it's important to test the circuit on a breadboard. If you don't want to make a PCB, you can still follow this project by assembling the circuit on a breadboard.
Parts Required
To assemble the circuit on a breadboard you need the following parts (the parts for the actual PCB are shown in a later section):
DOIT ESP32 DEVKIT V1 Board read Best ESP32 Development Boards
BME280 (4 pins)
I2C OLED Display (4 pins)
Light dependent resistor
Pushbutton
2x 10k Ohm resistor
Breadboard
Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
After gathering all the parts, assemble the circuit by following the next schematic diagram:
Designing the PCB
To design the circuit and PCB, we used EasyEDA which is a browser based software to design PCBs. If you want to customize your PCB, you just need to upload the following files:
EasyEDA project files to edit the PCB
Designing the circuit works like in any other circuit software tool, you place some components and you wire them together. Then, you assign each component to a footprint.
Having the parts assigned, place each component. When you're happy with the layout, make all the connections and route your PCB.
Save your project and export the Gerber files.
Note: you can grab the project files and edit them to customize the shield for your own needs.
Download Gerber .zip file
EasyEDA project to edit the PCB
Ordering the PCBs at PCBWay
This project is sponsored by PCBWay. PCBWay is a full feature Printed Circuit Board manufacturing service.
Turn your DIY breadboard circuits into professional PCBs get 10 boards for approximately $5 + shipping (which will vary depending on your country).
Once you have your Gerber files, you can order the PCB. Follow the next steps.
1. Download the Gerber files click here to download the .zip file
2. Go to PCBWay website and open the PCB Instant Quote page.
3. PCBWay can grab all the PCB details and automatically fills them for you. Use the Quick-order PCB (Autofill parameters).
4. Press the + Add Gerber file button to upload the provided Gerber files.
And that's it. You can also use the OnlineGerberViewer to check if your PCB is looking as it should.
If you aren't in a hurry, you can use the China Post shipping method to lower your cost significantly. In our opinion, we think they overestimate the China Post shipping time.
You can increase your PCB order quantity and change the solder mask color. I've ordered the Blue color.
Once you're ready, you can order the PCBs by clicking Save to Cart and complete your order.
Unboxing
After approximately one week using the DHL shipping method, I received the PCBs at my office.
As usual, everything comes well packed, and the PCBs are really high-quality. The letters on the silkscreen are really well-printed and easy to read. Additionally, the solder sticks easily to the pads.
We're really satisfied with the PCBWay service. Here's some other projects we've built using the PCBWay service:
ESP32-CAM with Telegram: Take Photos, Control Outputs, Request Sensor Readings and Motion Notifications
ESP32 IoT Shield PCB with Dashboard for Outputs and Sensors
Soldering the Components
The next step is soldering the components to the PCB. I've used SMD LEDs, SMD resistors and SMD capacitors. These can be a bit difficult to solder, but the PCB looks much better.
If you've never soldered SMD before, we recommend watching a few videos to learn how it's done. You can also get an SMD DIY soldering Kit to practice a bit.
Here's a list of all the components needed to assemble the PCB:
DOIT ESP32 DEVKIT V1 Board (36 GPIOs)
12x SMD WS2812B addressable RGB LEDs
2x 10k Ohm SMD resistor (1206)
12x 10nF capacitors (0805)
Pushbutton (0.55 mm)
Female pin header socket (2.54 mm)
BME280 (4 pins)
Light dependent resistor
I2C SSD1306 0.96inch OLED display (4 pins)
Here's the soldering tools I've used:
TS80 mini portable soldering iron
Solder 60/40 0.5mm diameter
Soldering mat
Read our review about the TS80 Soldering Iron: TS80 Soldering Iron Review Best Portable Soldering Iron.
Start by soldering the SMD components. Then, solder the header pins. And finally, solder the other components or use header pins if you don't want to connect the components permanently.
Here's how the ESP32 Shield looks like after assembling all the parts.
The ESP32 board should stack perfectly on the header pins on the other side of the PCB.
Programming the Weather Station Interface PCB
The code for this project displays sensors readings on different screens on the OLED display as well as date and time. The addressable RGB LEDs show different colors and animations accordingly to what is displayed on the screen.
You can program the PCB in any way that is more suitable for you.
We'll program the ESP32 board using Arduino IDE. So make sure you have the ESP32 board add-on installed.
Installing ESP32 Board in Arduino IDE (Windows, Mac OS X, and Linux)
If you want to program the ESP32/ESP8266 using VS Code + PlatformIO, follow the next tutorial:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
Installing Libraries (Arduino IDE)
For this project, you need to install all these libraries in your Arduino IDE.
Adafruit Neopixel
Adafruit GFX
Adafruit SSD1306
Adafruit_BME280 library
Adafruit_Sensor library
All these libraries can be installed using the Arduino IDE library manager. Just go to Sketch > Include Library > Manage Libraries and search for the library name.
Installing Libraries (VS Code + PlatformIO)
If you're programming the ESP32 using PlatformIO, you should include the libraries on the platformio.ini file like this:
[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
monitor_speed = 115200
lib_deps = adafruit/Adafruit NeoPixel @ ^1.7.0
adafruit/Adafruit SSD1306 @ ^2.4.1
adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/Adafruit BME280 Library @ ^2.1.2
adafruit/Adafruit GFX Library @ ^1.10.3
adafruit/Adafruit BusIO @ ^1.6.0
Code
Copy the following code to your Arduino IDE or to the main.cpp file if your using PlatformIO.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-weather-station-pcb/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <WiFi.h>
#include <time.h>
// Insert your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// NTP Server Details
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 0;
const int daylightOffset_sec = 3600;
// OLED Display
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define I2Cdisplay_SDA 21
#define I2Cdisplay_SCL 22
TwoWire I2Cdisplay = TwoWire(1);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &I2Cdisplay, -1);
// WS2812B Addressable RGB LEDs
#define LED_PIN 27 // GPIO the LEDs are connected to
#define LED_COUNT 12 // Number of LEDs
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
// BME280
#define I2C_SDA 21
#define I2C_SCL 22
TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;
// LDR (Light Dependent Resistor)
#define ldr 33
// Pushbutton
#define buttonPin 18
int buttonState; // current reading from the input pin
int lastButtonState = LOW; // previous reading from the input pin
unsigned long lastDebounceTime = 0; // the last time the output pin was toggled
unsigned long debounceDelay = 50; // the debounce time; increase if the output flickers
// Screens
int displayScreenNum = 0;
int displayScreenNumMax = 4;
unsigned long lastTimer = 0;
unsigned long timerDelay = 15000;
unsigned char temperature_icon[] ={
0b00000001, 0b11000000, // ###
0b00000011, 0b11100000, // #####
0b00000111, 0b00100000, // ### #
0b00000111, 0b11100000, // ######
0b00000111, 0b00100000, // ### #
0b00000111, 0b11100000, // ######
0b00000111, 0b00100000, // ### #
0b00000111, 0b11100000, // ######
0b00000111, 0b00100000, // ### #
0b00001111, 0b11110000, // ########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11111000, // ##########
0b00001111, 0b11110000, // ########
0b00000111, 0b11100000, // ######
};
unsigned char humidity_icon[] ={
0b00000000, 0b00000000, //
0b00000001, 0b10000000, // ##
0b00000011, 0b11000000, // ####
0b00000111, 0b11100000, // ######
0b00001111, 0b11110000, // ########
0b00001111, 0b11110000, // ########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11011000, // ####### ##
0b00111111, 0b10011100, // ####### ###
0b00111111, 0b10011100, // ####### ###
0b00111111, 0b00011100, // ###### ###
0b00011110, 0b00111000, // #### ###
0b00011111, 0b11111000, // ##########
0b00001111, 0b11110000, // ########
0b00000011, 0b11000000, // ####
0b00000000, 0b00000000, //
};
unsigned char arrow_down_icon[] ={
0b00001111, 0b11110000, // ########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11111000, // ##########
0b00011100, 0b00111000, // ### ###
0b00011100, 0b00111000, // ### ###
0b00011100, 0b00111000, // ### ###
0b01111100, 0b00111110, // ##### #####
0b11111100, 0b00111111, // ###### ######
0b11111100, 0b00111111, // ###### ######
0b01111000, 0b00011110, // #### ####
0b00111100, 0b00111100, // #### ####
0b00011110, 0b01111000, // #### ####
0b00001111, 0b11110000, // ########
0b00000111, 0b11100000, // ######
0b00000011, 0b11000000, // ####
0b00000001, 0b10000000, // ##
};
unsigned char sun_icon[] ={
0b00000000, 0b00000000, //
0b00100000, 0b10000010, // # # #
0b00010000, 0b10000100, // # # #
0b00001000, 0b00001000, // # #
0b00000001, 0b11000000, // ###
0b00000111, 0b11110000, // #######
0b00000111, 0b11110000, // #######
0b00001111, 0b11111000, // #########
0b01101111, 0b11111011, // ## ######### ##
0b00001111, 0b11111000, // #########
0b00000111, 0b11110000, // #######
0b00000111, 0b11110000, // #######
0b00010001, 0b11000100, // # ### #
0b00100000, 0b00000010, // # #
0b01000000, 0b10000001, // # # #
0b00000000, 0b10000000, // #
};
// Clear the LEDs
void colorWipe(uint32_t color, int wait, int numNeoPixels) {
for(int i=0; i<numNeoPixels; i++) { // For each pixel in strip...
strip.setPixelColor(i, color); // Set pixel's color (in RAM)
strip.show(); // Update strip to match
delay(wait); // Pause for a moment
}
}
// Rainbow cycle along all LEDs. Pass delay time (in ms) between frames.
void rainbow(int wait) {
long firstPixelHue = 256;
for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());
strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
}
strip.show(); // Update strip with new contents
delay(wait); // Pause for a moment
}
// Create display marker for each screen
void displayIndicator(int displayNumber) {
int xCoordinates[5] = {44, 54, 64, 74, 84};
for (int i =0; i<5; i++) {
if (i == displayNumber) {
display.fillCircle(xCoordinates[i], 60, 2, WHITE);
}
else {
display.drawCircle(xCoordinates[i], 60, 2, WHITE);
}
}
}
//SCREEN NUMBER 0: DATE AND TIME
void displayLocalTime(){
struct tm timeinfo;
if(!getLocalTime(&timeinfo)){
Serial.println("Failed to obtain time");
}
Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
//GET DATE
//Get full weekday name
char weekDay[10];
strftime(weekDay, sizeof(weekDay), "%a", &timeinfo);
//Get day of month
char dayMonth[4];
strftime(dayMonth, sizeof(dayMonth), "%d", &timeinfo);
//Get abbreviated month name
char monthName[5];
strftime(monthName, sizeof(monthName), "%b", &timeinfo);
//Get year
char year[6];
strftime(year, sizeof(year), "%Y", &timeinfo);
//GET TIME
//Get hour (12 hour format)
/*char hour[4];
strftime(hour, sizeof(hour), "%I", &timeinfo);*/
//Get hour (24 hour format)
char hour[4];
strftime(hour, sizeof(hour), "%H", &timeinfo);
//Get minute
char minute[4];
strftime(minute, sizeof(minute), "%M", &timeinfo);
//Display Date and Time on OLED display
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(3);
display.setCursor(19,5);
display.print(hour);
display.print(":");
display.print(minute);
display.setTextSize(1);
display.setCursor(16,40);
display.print(weekDay);
display.print(", ");
display.print(dayMonth);
display.print(" ");
display.print(monthName);
display.print(" ");
display.print(year);
displayIndicator(displayScreenNum);
display.display();
rainbow(10);
}
// SCREEN NUMBER 1: TEMPERATURE
void displayTemperature(){
display.clearDisplay();
display.setTextSize(2);
display.drawBitmap(15, 5, temperature_icon, 16, 16 ,1);
display.setCursor(35, 5);
float temperature = bme.readTemperature();
display.print(temperature);
display.cp437(true);
display.setTextSize(1);
display.print(" ");
display.write(167);
display.print("C");
display.setCursor(0, 34);
display.setTextSize(1);
display.print("Humidity: ");
display.print(bme.readHumidity());
display.print(" %");
display.setCursor(0, 44);
display.setTextSize(1);
display.print("Pressure: ");
display.print(bme.readPressure()/100.0F);
display.print(" hpa");
displayIndicator(displayScreenNum);
display.display();
int temperaturePer = map(temperature, -5, 36, 0, LED_COUNT-1);
colorWipe(strip.Color(0, 255, 0), 50, temperaturePer);
}
// SCREEN NUMBER 2: HUMIDITY
void displayHumidity(){
display.clearDisplay();
display.setTextSize(2);
display.drawBitmap(15, 5, humidity_icon, 16, 16 ,1);
display.setCursor(35, 5);
float humidity = bme.readHumidity();
display.print(humidity);
display.print(" %");
display.setCursor(0, 34);
display.setTextSize(1);
display.print("Temperature: ");
display.print(bme.readTemperature());
display.cp437(true);
display.print(" ");
display.write(167);
display.print("C");
display.setCursor(0, 44);
display.setTextSize(1);
display.print("Pressure: ");
display.print(bme.readPressure()/100.0F);
display.print(" hpa");
displayIndicator(displayScreenNum);
display.display();
int humidityPer = map(humidity, 0, 100, 0, LED_COUNT-1);
colorWipe(strip.Color(0, 0, 255), 50, humidityPer);
}
// SCREEN NUMBER 3: PRESSURE
void displayPressure(){
display.clearDisplay();
display.setTextSize(2);
display.drawBitmap(0, 5, arrow_down_icon, 16, 16 ,1);
display.setCursor(20, 5);
display.print(bme.readPressure()/100.0F);
display.setTextSize(1);
display.print(" hpa");
display.setCursor(0, 34);
display.setTextSize(1);
display.print("Temperature: ");
display.print(bme.readTemperature());
display.cp437(true);
display.print(" ");
display.write(167);
display.print("C");
display.setCursor(0, 44);
display.setTextSize(1);
display.print("Humidity: ");
display.print(bme.readHumidity());
display.print(" hpa");
displayIndicator(displayScreenNum);
display.display();
colorWipe(strip.Color(255, 0, 255), 50, 12);
}
// SCREEN NUMBER 4: LUMINOSITY
void displayLDR(){
display.clearDisplay();
display.setTextSize(2);
display.drawBitmap(33, 5, sun_icon, 16, 16 ,1);
display.setCursor(53, 5);
int ldrReading = map(analogRead(ldr), 0, 4095, 100, 0);
display.print(ldrReading);
display.print(" %");
display.setTextSize(1);
display.setCursor(0, 34);
display.print("Temperature: ");
display.print(bme.readTemperature());
display.print(" ");
display.cp437(true);
display.write(167);
display.print("C");
display.setCursor(0, 44);
display.setTextSize(1);
display.print("Humidity: ");
display.print(bme.readHumidity());
display.print(" %");
display.setCursor(0, 44);
displayIndicator(displayScreenNum);
display.display();
int ldrReadingPer = map(ldrReading, 0, 100, 0, LED_COUNT-1);
colorWipe(strip.Color(255, 255, 0), 50, ldrReadingPer);
}
// Display the right screen accordingly to the displayScreenNum
void updateScreen() {
colorWipe(strip.Color(0, 0, 0), 1, LED_COUNT);
if (displayScreenNum == 0){
displayLocalTime();
}
else if (displayScreenNum == 1) {
displayTemperature();
}
else if (displayScreenNum ==2){
displayHumidity();
}
else if (displayScreenNum==3){
displayPressure();
}
else {
displayLDR();
}
}
void setup() {
Serial.begin(115200);
// Initialize the pushbutton pin as an input
pinMode(buttonPin, INPUT);
I2CBME.begin(I2C_SDA, I2C_SCL, 100000);
I2Cdisplay.begin(I2Cdisplay_SDA, I2Cdisplay_SCL, 100000);
// Initialize OLED Display
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
display.setTextColor(WHITE);
// Initialize BME280
bool status = bme.begin(0x76, &I2CBME);
if (!status) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
// Initialize WS2812B LEDs
strip.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
strip.show(); // Turn OFF all pixels ASAP
strip.setBrightness(50); // Set BRIGHTNESS to about 1/5 (max = 255)
// Connect to Wi-Fi
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected.");
// Init and get the time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}
void loop() {
// read the state of the switch into a local variable
int reading = digitalRead(buttonPin);
// Change screen when the pushbutton is pressed
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != buttonState) {
buttonState = reading;
if (buttonState == HIGH) {
updateScreen();
Serial.println(displayScreenNum);
if(displayScreenNum < displayScreenNumMax) {
displayScreenNum++;
}
else {
displayScreenNum = 0;
}
lastTimer = millis();
}
}
}
lastButtonState = reading;
// Change screen every 15 seconds (timerDelay variable)
if ((millis() - lastTimer) > timerDelay) {
updateScreen();
Serial.println(displayScreenNum);
if(displayScreenNum < displayScreenNumMax) {
displayScreenNum++;
}
else {
displayScreenNum = 0;
}
lastTimer = millis();
}
}
View raw code
We get date and time from an NTP server. So, the ESP32 needs to connect to the internet. Insert your network credentials on the following variables and the code will work straight away.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
How the Code Works
Read this section if you want to learn how the code works, or skip to the next section.
This code is quite long, but simple. If you want to fully understand how it works, you may need to take a look at the following tutorials:
ESP32 NTP Client-Server: Get Date and Time (Arduino IDE)
ESP32 OLED Display with Arduino IDE
ESP32/ESP8266: DHT Temperature and Humidity Readings in OLED Display
ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity)
Guide for WS2812B Addressable RGB LED Strip with Arduino
Including Libraries
First, you need to include the necessary libraries.
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <WiFi.h>
#include <time.h>
Network Credentials
Insert your network credentials in the following lines so that the ESP32 can connect to your network to request date and time from an NTP server.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
NTP Server
Then, you need to define the following variables to configure and get time from an NTP server: ntpServer, gmtOffset_sec and daylightOffset_sec.
We'll request the time from pool.ntp.org, which is a cluster of timeservers that anyone can use to request the time.
const char* ntpServer = "pool.ntp.org";
GMT Offset
The gmtOffset_sec variable defines the offset in seconds between your time zone and GMT. We live in Portugal, so the time offset is 0. Change the time gmtOffset_sec variable to match your time zone.
const long gmtOffset_sec = 0;
Daylight Offset
The daylightOffset_sec variable defines the offset in seconds for daylight saving time. It is generally one hour, that corresponds to 3600 seconds
const int daylightOffset_sec = 3600;
Learn more about getting time from NTP server: ESP32 NTP Client-Server: Get Date and Time (Arduino IDE)
OLED Display
The SCREEN_WIDTH and SCREEN_HEIGHT variables define the dimensions of the OLED display in pixels. We're using a 0.96inch OLED display: 128 x 64 pixels.
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
The OLED display is connected to GPIO 22 (SCL) and GPIO 21 (SDA).
#define I2Cdisplay_SDA 21
#define I2Cdisplay_SCL 22
TwoWire I2Cdisplay = TwoWire(1);
Initialize a display object with the width and height defined earlier with I2C communication protocol (&I2Cdisplay).
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &I2Cdisplay, -1);
WS2812B Addressable RGB LEDS
You need to define the GPIO that the RGB LEDs are connected to. Addressable RGB LEDs communicate with the ESP32 using One Wire protocol. So, all LEDs can be controlled by the same GPIO. In this case, they are connected to GPIO 27.
#define LED_PIN 27
The LED_COUNT variable saves the number of addressable RGB LEDs we want to control. In this case, it's 12.
#define LED_COUNT 12
Create an Adafruit_NeoPixel object called strip to control the addressable RGB LEDs.
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
BME280 Sensor
Create an Adafruit_BME280 object called bme on the default ESP32 pins.
#define I2C_SDA 21
#define I2C_SCL 22
TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;
LDR
Define the GPIO the LDR is connected to.
const int ldr = 33; // LDR (Light Dependent Resistor)
Pushbutton
Define the GPIO the pushbutton is connected to.
#define buttonPin 18
The following variables are used to handle the pushbutton.
int buttonState; // current reading from the input pin
int lastButtonState = LOW; // previous reading from the input pin
unsigned long lastDebounceTime = 0; // the last time the output pin was toggled
unsigned long debounceDelay = 50; // the debounce time; increase if the output flickers
OLED Screens
As mentioned previously, the OLED will display five different screens. Each screen is numbered from 0 to 4. The displayScreenNum variable holds the screen number that must be displayed on the OLED it starts at zero. The displayNumMax holds the maximum number of screens.
int displayScreenNum = 0;
int displayScreenNumMax = 4;
The next variables will be used to handle the timer to display each screen for 15 seconds. You can change the display screen period on the timerDelay variable.
unsigned long lastTimer = 0;
unsigned long timerDelay = 15000;
Icons
In each screen, the OLED displays an icon related with the reading it is showing. In the temperature screen it displays a thermometer (temperature_icon), in the humidity screen a teardrop (humidity_icon), in the pressure screen a arrow (arrow_down_icon) and in the luminosity screen a sun (sun_icon). We need to include those icons in the code. These icons are 1616 pixels.
unsigned char temperature_icon[] ={
0b00000001, 0b11000000, // ###
0b00000011, 0b11100000, // #####
0b00000111, 0b00100000, // ### #
0b00000111, 0b11100000, // ######
0b00000111, 0b00100000, // ### #
0b00000111, 0b11100000, // ######
0b00000111, 0b00100000, // ### #
0b00000111, 0b11100000, // ######
0b00000111, 0b00100000, // ### #
0b00001111, 0b11110000, // ########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11111000, // ##########
0b00001111, 0b11110000, // ########
0b00000111, 0b11100000, // ######
};
unsigned char humidity_icon[] ={
0b00000000, 0b00000000, //
0b00000001, 0b10000000, // ##
0b00000011, 0b11000000, // ####
0b00000111, 0b11100000, // ######
0b00001111, 0b11110000, // ########
0b00001111, 0b11110000, // ########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11011000, // ####### ##
0b00111111, 0b10011100, // ####### ###
0b00111111, 0b10011100, // ####### ###
0b00111111, 0b00011100, // ###### ###
0b00011110, 0b00111000, // #### ###
0b00011111, 0b11111000, // ##########
0b00001111, 0b11110000, // ########
0b00000011, 0b11000000, // ####
0b00000000, 0b00000000, //
};
unsigned char arrow_down_icon[] ={
0b00001111, 0b11110000, // ########
0b00011111, 0b11111000, // ##########
0b00011111, 0b11111000, // ##########
0b00011100, 0b00111000, // ### ###
0b00011100, 0b00111000, // ### ###
0b00011100, 0b00111000, // ### ###
0b01111100, 0b00111110, // ##### #####
0b11111100, 0b00111111, // ###### ######
0b11111100, 0b00111111, // ###### ######
0b01111000, 0b00011110, // #### ####
0b00111100, 0b00111100, // #### ####
0b00011110, 0b01111000, // #### ####
0b00001111, 0b11110000, // ########
0b00000111, 0b11100000, // ######
0b00000011, 0b11000000, // ####
0b00000001, 0b10000000, // ##
};
unsigned char sun_icon[] ={
0b00000000, 0b00000000, //
0b00100000, 0b10000010, // # # #
0b00010000, 0b10000100, // # # #
0b00001000, 0b00001000, // # #
0b00000001, 0b11000000, // ###
0b00000111, 0b11110000, // #######
0b00000111, 0b11110000, // #######
0b00001111, 0b11111000, // #########
0b01101111, 0b11111011, // ## ######### ##
0b00001111, 0b11111000, // #########
0b00000111, 0b11110000, // #######
0b00000111, 0b11110000, // #######
0b00010001, 0b11000100, // # ### #
0b00100000, 0b00000010, // # #
0b01000000, 0b10000001, // # # #
0b00000000, 0b10000000, // #
};
To learn more about how to display icons, we recommend reading our OLED guide: ESP32 OLED Display with Arduino IDE.
colorWipe() and rainbow() functions
The colorWipe() and rainbow() functions are used to control the addresable RGB LEDs.
The colorWipe() is used to light up or clear specific LEDs.
void colorWipe(uint32_t color, int wait, int numNeoPixels) {
for(int i=0; i<numNeoPixels; i++) { // For each pixel in strip...
strip.setPixelColor(i, color); // Set pixel's color (in RAM)
strip.show(); // Update strip to match
delay(wait); // Pause for a moment
}
}
The rainbow() function, as the name suggests, displays a rainbow effect.
// Rainbow cycle along all LEDs. Pass delay time (in ms) between frames.
void rainbow(int wait) {
long firstPixelHue = 256;
for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());
strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
}
strip.show(); // Update strip with new contents
delay(wait); // Pause for a moment
}
You can learn more about these and other functions to control the strip by taking a look at the Neopixel Library's examples.
displayIndicator() function
The displayIndicator() creates five little circles at the bottom of the display accordingly to the screen that is being displayed at the moment.
// Create display marker for each screen
void displayIndicator(int displayNumber) {
int xCoordinates[5] = {44, 54, 64, 74, 84};
for (int i =0; i<5; i++) {
if (i == displayNumber) {
display.fillCircle(xCoordinates[i], 60, 2, WHITE);
}
else {
display.drawCircle(xCoordinates[i], 60, 2, WHITE);
}
}
}
The drawCircle(x, y, radius, color) function creates a circle. The fillCircle(x, y, radius, color) creates a filled circle.
We're placing the center of the circles on the following x coordinates: 44, 54, 64, 74 and 84.
int xCoordinates[5] = {44, 54, 64, 74, 84};
We draw a filled circle for the current display and a empty circle for the other displays:
if (i == displayNumber) {
display.fillCircle(xCoordinates[i], 60, 2, WHITE);
}
else {
display.drawCircle(xCoordinates[i], 60, 2, WHITE);
}
Screen Number 0: Date and Time
The first screen that shows up on the OLED displays date and time. That's what the displayLocalTime() function does. It also displays a rainbow effect on the addressable RGB LEDs.
//SCREEN NUMBER 0: DATE AND TIME
void displayLocalTime(){
struct tm timeinfo;
if(!getLocalTime(&timeinfo)){
Serial.println("Failed to obtain time");
}
Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
//GET DATE
//Get full weekday name
char weekDay[10];
strftime(weekDay, sizeof(weekDay), "%a", &timeinfo);
//Get day of month
char dayMonth[4];
strftime(dayMonth, sizeof(dayMonth), "%d", &timeinfo);
//Get abbreviated month name
char monthName[5];
strftime(monthName, sizeof(monthName), "%b", &timeinfo);
//Get year
char year[6];
strftime(year, sizeof(year), "%Y", &timeinfo);
//GET TIME
//Get hour (12 hour format)
/*char hour[4];
strftime(hour, sizeof(hour), "%I", &timeinfo);*/
//Get hour (24 hour format)
char hour[4];
strftime(hour, sizeof(hour), "%H", &timeinfo);
//Get minute
char minute[4];
strftime(minute, sizeof(minute), "%M", &timeinfo);
//Display Date and Time on OLED display
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(3);
display.setCursor(19,5);
display.print(hour);
display.print(":");
display.print(minute);
display.setTextSize(1);
display.setCursor(16,40);
display.print(weekDay);
display.print(", ");
display.print(dayMonth);
display.print(" ");
display.print(monthName);
display.print(" ");
display.print(year);
displayIndicator(displayScreenNum);
display.display();
rainbow(10);
}
Learn more about getting date and time from an NTP server with the ESP32: ESP32 NTP Client-Server: Get Date and Time (Arduino IDE)
Screen Number 1: Temperature
The second screen displays temperature. That's done by calling the displayTemperature() function. This function also lights up the LEDs in a green color accordingly to the temperature value.
// SCREEN NUMBER 1: TEMPERATURE
void displayTemperature(){
display.clearDisplay();
display.setTextSize(2);
display.drawBitmap(15, 5, temperature_icon, 16, 16 ,1);
display.setCursor(35, 5);
float temperature = bme.readTemperature();
display.print(temperature);
display.cp437(true);
display.setTextSize(1);
display.print(" ");
display.write(167);
display.print("C");
display.setCursor(0, 34);
display.setTextSize(1);
display.print("Humidity: ");
display.print(bme.readHumidity());
display.print(" %");
display.setCursor(0, 44);
display.setTextSize(1);
display.print("Pressure: ");
display.print(bme.readPressure()/100.0F);
display.print(" hpa");
displayIndicator(displayScreenNum);
display.display();
int temperaturePer = map(temperature, -5, 36, 0, LED_COUNT-1);
colorWipe(strip.Color(0, 255, 0), 50, temperaturePer);
}
Screen Number 2: Humidity
The displayHumidity() function is similar to the displayTemperature() function but displays the humidity value.
// SCREEN NUMBER 2: HUMIDITY
void displayHumidity(){
display.clearDisplay();
display.setTextSize(2);
display.drawBitmap(15, 5, humidity_icon, 16, 16 ,1);
display.setCursor(35, 5);
float humidity = bme.readHumidity();
display.print(humidity);
display.print(" %");
display.setCursor(0, 34);
display.setTextSize(1);
display.print("Temperature: ");
display.print(bme.readTemperature());
display.cp437(true);
display.print(" ");
display.write(167);
display.print("C");
display.setCursor(0, 44);
display.setTextSize(1);
display.print("Pressure: ");
display.print(bme.readPressure()/100.0F);
display.print(" hpa");
displayIndicator(displayScreenNum);
display.display();
int humidityPer = map(humidity, 0, 100, 0, LED_COUNT-1);
colorWipe(strip.Color(0, 0, 255), 50, humidityPer);
}
Screen Number 3: Pressure
The displayPressure() function is similar to the two previous functions, but display the pressure value.
// SCREEN NUMBER 3: PRESSURE
void displayPressure(){
display.clearDisplay();
display.setTextSize(2);
display.drawBitmap(0, 5, arrow_down_icon, 16, 16 ,1);
display.setCursor(20, 5);
display.print(bme.readPressure()/100.0F);
display.setTextSize(1);
display.print(" hpa");
display.setCursor(0, 34);
display.setTextSize(1);
display.print("Temperature: ");
display.print(bme.readTemperature());
display.cp437(true);
display.print(" ");
display.write(167);
display.print("C");
display.setCursor(0, 44);
display.setTextSize(1);
display.print("Humidity: ");
display.print(bme.readHumidity());
display.print(" hpa");
displayIndicator(displayScreenNum);
display.display();
colorWipe(strip.Color(255, 0, 255), 50, 12);
}
Screen Number 4: Luminosity
Finally, the last screen displays the luminosity (displayLDR() function).
void displayLDR(){
display.clearDisplay();
display.setTextSize(2);
display.drawBitmap(33, 5, sun_icon, 16, 16 ,1);
display.setCursor(53, 5);
int ldrReading = map(analogRead(ldr), 0, 4095, 100, 0);
display.print(ldrReading);
display.print(" %");
display.setTextSize(1);
display.setCursor(0, 34);
display.print("Temperature: ");
display.print(bme.readTemperature());
display.print(" ");
display.cp437(true);
display.write(167);
display.print("C");
display.setCursor(0, 44);
display.setTextSize(1);
display.print("Humidity: ");
display.print(bme.readHumidity());
display.print(" %");
display.setCursor(0, 44);
displayIndicator(displayScreenNum);
display.display();
int ldrReadingPer = map(ldrReading, 0, 100, 0, LED_COUNT-1);
colorWipe(strip.Color(255, 255, 0), 50, ldrReadingPer);
}
updateScreen() function
The updateScreen() function calls the right functions accordingly to the screen we want to display:
void updateScreen() {
colorWipe(strip.Color(0, 0, 0), 1, LED_COUNT);
if (displayScreenNum == 0){
displayLocalTime();
}
else if (displayScreenNum == 1) {
displayTemperature();
}
else if (displayScreenNum ==2){
displayHumidity();
}
else if (displayScreenNum==3){
displayPressure();
}
else {
displayLDR();
}
}
setup()
Set the button as an input.
pinMode(buttonPin, INPUT);
Initialize the OLED display.
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
Initialize the BME280 sensor:
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
When the ESP32 first starts, we want to clear the OLED display. We also set the text color to white.
display.clearDisplay();
display.setTextColor(WHITE);
Initialize the WS2812B LEDs and set their brightness. You can change the brightness to any other value that best suits your enviroment.
strip.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
strip.show(); // Turn OFF all pixels ASAP
strip.setBrightness(50); // Set BRIGHTNESS to about 1/5 (max = 255)
Initialize Wi-Fi and connect the ESP32 to your local network, so that the ESP32 can connect to the NTP server to get date and time.
// Connect to Wi-Fi
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected.");
Configure the NTP Server with the settings defined earlier.
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
printLocalTime();
loop()
In the loop(), the following lines change the screen (displayScreenNum) every time the pushbutton is pressed.
// read the state of the switch into a local variable
int reading = digitalRead(buttonPin);
// Change screen when the pushbutton is pressed
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != buttonState) {
buttonState = reading;
if (buttonState == HIGH) {
updateScreen();
Serial.println(displayScreenNum);
if(displayScreenNum < displayScreenNumMax) {
displayScreenNum++;
}
else {
displayScreenNum = 0;
}
lastTimer = millis();
}
}
}
lastButtonState = reading;
The next lines change between screens every 15 seconds (timerDelay).
if ((millis() - lastTimer) > timerDelay) {
updateScreen();
Serial.println(displayScreenNum);
if(displayScreenNum < displayScreenNumMax) {
displayScreenNum++;
}
else {
displayScreenNum = 0;
}
lastTimer = millis();
}
Demonstration
After uploading the code to the board, press the on-board RESET button so that the ESP32 starts running the code.
For a complete demonstration, we recommend watching the following video.
Wrapping Up
We hope you've found this project interesting and you're able to build it yourself. You can use PCBWay service and you'll get a high quality PCB for your projects.
You can program the ESP32 with other code suitable for your needs. You can also edit the gerber files and add other features to the PCB or other sensors.
ESP-MESH with ESP32 and ESP8266: Getting Started (painlessMesh library)
Learn how to use ESP-MESH networking protocol to build a mesh network with the ESP32 and ESP8266 NodeMCU boards. ESP-MESH allows multiple devices (nodes) to communicate with each other under a single wireless local area network. It is supported on the ESP32 and ESP8266 boards. In this guide, we'll show you how to get started with ESP-MESH using the Arduino core.
This article covers the following topics:
Introducing ESP-MESH
ESP-MESH Basic Example (Broadcast messages)
Exchange Sensor Readings using ESP-MESH (broadcast)
Arduino IDE
If you want to program the ESP32 and ESP8266 boards using Arduino IDE, you should have the ESP32 or ESP8266 add-ons installed. Follow the next guides:
Installing ESP32 Board in Arduino IDE (Windows, Mac OS X, and Linux)
Installing ESP8266 Board in Arduino IDE (Windows, Mac OS X, Linux)
If you want to program the ESP32/ESP8266 using VS Code + PlatformIO, follow the next tutorial:
Getting Started with VS Code and PlatformIO IDE for ESP32 and ESP8266 (Windows, Mac OS X, Linux Ubuntu)
Introducing ESP-MESH
Accordingly to the Espressif documentation:
ESP-MESH is a networking protocol built atop the Wi-Fi protocol. ESP-MESH allows numerous devices (referred to as nodes) spread over a large physical area (both indoors and outdoors) to be interconnected under a single WLAN (Wireless Local-Area Network).
ESP-MESH is self-organizing and self-healing meaning the network can be built and maintained autonomously. For more information, visit the ESP-MESH official documentation.
Traditional Wi-Fi Network Architecture
In a traditional Wi-Fi network architecture, a single node (access point usually the router) is connected to all other nodes (stations). Each node can communicate with each other using the access point. However, this is limited to the access point wi-fi coverage. Every station must be in the range to connect directly to the access point. This doesn't happen with ESP-MESH.
ESP-MESH Network Architecture
With ESP-MESH, the nodes don't need to connect to a central node. Nodes are responsible for relaying each others transmissions. This allows multiple devices to spread over a large physical area. The Nodes can self-organize and dynamically talk to each other to ensure that the packet reaches its final node destination. If any node is removed from the network, it is able to self-organize to make sure that the packets reach their destination.
painlessMesh Library
The painlessMesh library allows us to create a mesh network with the ESP8266 or/and ESP32 boards in an easy way.
painlessMesh is a true ad-hoc network, meaning that no-planning, central controller, or router is required. Any system of 1 or more nodes will self-organize into fully functional mesh. The maximum size of the mesh is limited (we think) by the amount of memory in the heap that can be allocated to the sub-connections buffer and so should be really quite high. More information about the painlessMesh library.
Installing painlessMesh Library
You can install painlessMesh through the Arduino Library manager. Go to Tools > Manage Libraries. The Library Manager should open.
Search for painlessmesh and install the library. We're using Version 1.4.5
This library needs some other library dependencies. A new window should pop up asking you to install any missing dependencies. Select Install all.
If this window doesn't show up, you'll need to install the following library dependencies:
ArduinoJson (by bblanchon)
TaskScheduler
ESPAsyncTCP (ESP8266)
AsyncTCP (ESP32)
If you're using PlatformIO, add the following lines to the platformio.ini file to add the libraries and change the monitor speed.
For the ESP32:
monitor_speed = 115200
lib_deps = painlessmesh/painlessMesh @ ^1.4.5
ArduinoJson
arduinoUnity
TaskScheduler
AsyncTCP
For the ESP8266:
monitor_speed = 115200
lib_deps = painlessmesh/painlessMesh @ ^1.4.5
ArduinoJson
TaskScheduler
ESPAsyncTCP
ESP-MESH Basic Example (Broadcast messages)
To get started with ESP-MESH, we'll first experiment with the library's basic example. This example creates a mesh network in which all boards broadcast messages to all the other boards.
We've experimented this example with four boards (two ESP32 and two ESP8266). You can add or remove boards. The code is compatible with both the ESP32 and ESP8266 boards.
Code painlessMesh Library Basic Example
Copy the following code to your Arduino IDE (code from the library examples). The code is compatible with both the ESP32 and ESP8266 boards.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp-mesh-esp32-esp8266-painlessmesh/
This is a simple example that uses the painlessMesh library: https://github.com/gmag11/painlessMesh/blob/master/examples/basic/basic.ino
*/
#include "painlessMesh.h"
#define MESH_PREFIX "whateverYouLike"
#define MESH_PASSWORD "somethingSneaky"
#define MESH_PORT 5555
Scheduler userScheduler; // to control your personal task
painlessMesh mesh;
// User stub
void sendMessage() ; // Prototype so PlatformIO doesn't complain
Task taskSendMessage( TASK_SECOND * 1 , TASK_FOREVER, &sendMessage );
void sendMessage() {
String msg = "Hi from node1";
msg += mesh.getNodeId();
mesh.sendBroadcast( msg );
taskSendMessage.setInterval( random( TASK_SECOND * 1, TASK_SECOND * 5 ));
}
// Needed for painless library
void receivedCallback( uint32_t from, String &msg ) {
Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str());
}
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("--> startHere: New Connection, nodeId = %u\n", nodeId);
}
void changedConnectionCallback() {
Serial.printf("Changed connections\n");
}
void nodeTimeAdjustedCallback(int32_t offset) {
Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset);
}
void setup() {
Serial.begin(115200);
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
mesh.init( MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT );
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
userScheduler.addTask( taskSendMessage );
taskSendMessage.enable();
}
void loop() {
// it will run the user scheduler as well
mesh.update();
}
View raw code
Before uploading the code, you can set up the MESH_PREFIX (it's like the name of the MESH network) and the MESH_PASSWORD variables (you can set it to whatever you like).
Then, we recommend that you change the following line for each board to easily identify the node that sent the message. For example, for node 1, change the message as follows:
String msg = "Hi from node 1 ";
How the Code Works
Start by including the painlessMesh library.
#include "painlessMesh.h"
MESH Details
Then, add the mesh details. The MESH_PREFIX refers to the name of the mesh. You can change it to whatever you like.
#define MESH_PREFIX "whateverYouLike"
The MESH_PASSWORD, as the name suggests is the mesh password. You can change it to whatever you like.
#define MESH_PASSWORD "somethingSneaky"
All nodes in the mesh should use the same MESH_PREFIX and MESH_PASSWORD.
The MESH_PORT refers to the the TCP port that you want the mesh server to run on. The default is 5555.
#define MESH_PORT 5555
Scheduler
It is recommended to avoid using delay() in the mesh network code. To maintain the mesh, some tasks need to be performed in the background. Using delay() will stop these tasks from happening and can cause the mesh to lose stability/fall apart.
Instead, it is recommended to use TaskScheduler to run your tasks which is used in painlessMesh itself.
The following line creates a new Scheduler called userScheduler.
Scheduler userScheduler; // to control your personal task
painlessMesh
Create a painlessMesh object called mesh to handle the mesh network.
Create tasks
Create a task called taskSendMessage responsible for calling the sendMessage() function every second as long as the program is running.
Task taskSendMessage(TASK_SECOND * 1 , TASK_FOREVER, &sendMessage);
Send a Message to the Mesh
The sendMessage() function sends a message to all nodes in the message network (broadcast).
void sendMessage() {
String msg = "Hi from node 1";
msg += mesh.getNodeId();
mesh.sendBroadcast( msg );
taskSendMessage.setInterval(random(TASK_SECOND * 1, TASK_SECOND * 5));
}
The message contains the Hi from node 1 text followed by the board chip ID.
String msg = "Hi from node 1";
msg += mesh.getNodeId();
To broadcast a message, simply use the sendBroadcast() method on the mesh object and pass as argument the message (msg) you want to send.
mesh.sendBroadcast(msg);
Every time a new message is sent, the code changes the interval between messages (one to five seconds).
taskSendMessage.setInterval(random(TASK_SECOND * 1, TASK_SECOND * 5));
Mesh Callback Functions
Next, several callback functions are created that will be called when specific events happen on the mesh.
The receivedCallback() function prints the message sender (from) and the content of the message (msg.c_str()).
void receivedCallback( uint32_t from, String &msg ) {
Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str());
}
The newConnectionCallback() function runs whenever a new node joins the network. This function simply prints the chip ID of the new node. You can modify the function to do any other task.
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("--> startHere: New Connection, nodeId = %u\n", nodeId);
}
The changedConnectionCallback() function runs whenever a connection changes on the network (when a node joins or leaves the network).
void changedConnectionCallback() {
Serial.printf("Changed connections\n");
}
The nodeTimeAdjustedCallback() function runs when the network adjusts the time, so that all nodes are synchronized. It prints the offset.
void nodeTimeAdjustedCallback(int32_t offset) {
Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset);
}
setup()
In the setup(), initialize the serial monitor.
void setup() {
Serial.begin(115200);
Choose the desired debug message types:
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
Initialize the mesh with the details defined earlier.
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
Assign all the callback functions to their corresponding events.
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
Finally, add the taskSendMessage function to the userScheduler. The scheduler is responsible for handling and running the tasks at the right time.
userScheduler.addTask(taskSendMessage);
Finally, enable the taskSendMessage, so that the program starts sending the messages to the mesh.
taskSendMessage.enable();
To keep the mesh running, add mesh.update() to the loop().
void loop() {
// it will run the user scheduler as well
mesh.update();
}
Demonstration
Upload the code provided to all your boards. Don't forget to modify the message to easily identify the sender node
With the boards connected to your computer, open a serial connection with each board. You can use the Serial Monitor, or you can use a software like PuTTY and open multiple windows for all the boards.
You should see that all boards receive each others messages. For example, these are the messages received by Node 1. It receives the messages from Node 2, 3 and 4.
You should also see other messages when there are changes on the mesh: when a board leaves or joins the network.
Exchange Sensor Readings using ESP-MESH
In this next example, we'll exchange sensor readings between 4 boards (you can use a different number of boards). Every board receives the other boards' readings.
As an example, we'll exchange sensor readings from a BME280 sensor, but you can use any other sensor.
Parts Required
Here's the parts required for this example:
4x ESP boards (ESP32 or ESP8266)
4x BME280
Breadboard
Jumper wires
Arduino_JSON library
In this example, we'll exchange the sensor readings in JSON format. To make it easier to handle JSON variables, we'll use the Arduino_JSON library.
You can install this library in the Arduino IDE Library Manager. Just go to Sketch > Include Library > Manage Libraries and search for the library name as follows:
If you're using VS Code with PlatformIO, include the libraries in the platformio.ini file as follows:
ESP32
monitor_speed = 115200
lib_deps = painlessmesh/painlessMesh @ ^1.4.5
ArduinoJson
arduinoUnity
AsyncTCP
TaskScheduler
adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/Adafruit BME280 Library @ ^2.1.2
arduino-libraries/Arduino_JSON @ ^0.1.0
ESP8266
monitor_speed = 115200
lib_deps = painlessmesh/painlessMesh @ ^1.4.5
ArduinoJson
TaskScheduler
ESPAsyncTCP
adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/Adafruit BME280 Library @ ^2.1.2
arduino-libraries/Arduino_JSON @ ^0.1.0
Circuit Diagram
Wire the BME280 sensor to the ESP32 or ESP8266 default I2C pins as shown in the following schematic diagrams.
ESP32
Recommended reading: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity)
ESP8266 NodeMCU
Recommended reading: ESP8266 with BME280 using Arduino IDE (Pressure, Temperature, Humidity)
Code ESP-MESH Broadcast Sensor Readings
Upload the following code to each of your boards. This code reads and broadcasts the current temperature, humidity and pressure readings to all boards on the mesh network. The readings are sent as a JSON string that also contains the node number to identify the sender board.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp-mesh-esp32-esp8266-painlessmesh/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include "painlessMesh.h"
#include <Arduino_JSON.h>
// MESH Details
#define MESH_PREFIX "RNTMESH" //name for your MESH
#define MESH_PASSWORD "MESHpassword" //password for your MESH
#define MESH_PORT 5555 //default port
//BME object on the default I2C pins
Adafruit_BME280 bme;
//Number for this node
int nodeNumber = 2;
//String to send to other nodes with sensor readings
String readings;
Scheduler userScheduler; // to control your personal task
painlessMesh mesh;
// User stub
void sendMessage() ; // Prototype so PlatformIO doesn't complain
String getReadings(); // Prototype for sending sensor readings
//Create tasks: to send messages and get readings;
Task taskSendMessage(TASK_SECOND * 5 , TASK_FOREVER, &sendMessage);
String getReadings () {
JSONVar jsonReadings;
jsonReadings["node"] = nodeNumber;
jsonReadings["temp"] = bme.readTemperature();
jsonReadings["hum"] = bme.readHumidity();
jsonReadings["pres"] = bme.readPressure()/100.0F;
readings = JSON.stringify(jsonReadings);
return readings;
}
void sendMessage () {
String msg = getReadings();
mesh.sendBroadcast(msg);
}
//Init BME280
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
// Needed for painless library
void receivedCallback( uint32_t from, String &msg ) {
Serial.printf("Received from %u msg=%s\n", from, msg.c_str());
JSONVar myObject = JSON.parse(msg.c_str());
int node = myObject["node"];
double temp = myObject["temp"];
double hum = myObject["hum"];
double pres = myObject["pres"];
Serial.print("Node: ");
Serial.println(node);
Serial.print("Temperature: ");
Serial.print(temp);
Serial.println(" C");
Serial.print("Humidity: ");
Serial.print(hum);
Serial.println(" %");
Serial.print("Pressure: ");
Serial.print(pres);
Serial.println(" hpa");
}
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("New Connection, nodeId = %u\n", nodeId);
}
void changedConnectionCallback() {
Serial.printf("Changed connections\n");
}
void nodeTimeAdjustedCallback(int32_t offset) {
Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset);
}
void setup() {
Serial.begin(115200);
initBME();
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
mesh.init( MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT );
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
userScheduler.addTask(taskSendMessage);
taskSendMessage.enable();
}
void loop() {
// it will run the user scheduler as well
mesh.update();
}
View raw code
The code is compatible with both the ESP32 and ESP8266 boards.
How the Code Works
Continue reading this section to learn how the code works.
Libraries
Start by including the required libraries: the Adafruit_Sensor and Adafruit_BME280 to interface with the BME280 sensor; the painlessMesh library to handle the mesh network and the Arduino_JSON to create and handle JSON strings easily.
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include "painlessMesh.h"
#include <Arduino_JSON.h>
Mesh details
Insert the mesh details in the following lines.
#define MESH_PREFIX "RNTMESH" //name for your MESH
#define MESH_PASSWORD "MESHpassword" //password for your MESH
#define MESH_PORT 5555 //default port
The MESH_PREFIX refers to the name of the mesh. You can change it to whatever you like. The MESH_PASSWORD, as the name suggests is the mesh password. You can change it to whatever you like. All nodes in the mesh should use the same MESH_PREFIX and MESH_PASSWORD.
The MESH_PORT refers to the the TCP port that you want the mesh server to run on. The default is 5555.
BME280
Create an Adafruit_BME280 object called bme on the default ESP32 or ESP8266 pins.
Adafruit_BME280 bme;
In the nodeNumber variable insert the node number for your board. It must be a different number for each board.
int nodeNumber = 2;
The readings variable will be used to save the readings to be sent to the other boards.
String readings;
Scheduler
The following line creates a new Scheduler called userScheduler.
Scheduler userScheduler; // to control your personal task
painlessMesh
Create a painlessMesh object called mesh to handle the mesh network.
Create tasks
Create a task called taskSendMessage responsible for calling the sendMessage() function every five seconds as long as the program is running.
Task taskSendMessage(TASK_SECOND * 5 , TASK_FOREVER, &sendMessage);
getReadings()
The getReadings() function gets temperature, humidity and pressure readings from the BME280 sensor and concatenates all the information, including the node number on a JSON variable called jsonReadings.
JSONVar jsonReadings;
jsonReadings["node"] = nodeNumber;
jsonReadings["temp"] = bme.readTemperature();
jsonReadings["hum"] = bme.readHumidity();
jsonReadings["pres"] = bme.readPressure()/100.0F;
The following line shows the structure of the jsonReadings variable with arbitrary values.
{
"node":2,
"temperature":24.51,
"humidity":52.01,
"pressure":1005.21
}
The jsonReadings variable is then converted into a JSON string using the stringify() method and saved on the readings variable.
readings = JSON.stringify(jsonReadings);
This variable is then returned by the function.
return readings;
Send a Message to the Mesh
The sendMessage() function sends the JSON string with the readings and node number (getReadings()) to all nodes in the network (broadcast).
void sendMessage () {
String msg = getReadings();
mesh.sendBroadcast(msg);
}
Init BME280 sensor
The initBME() function initializes the BME280 sensor.
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
Mesh Callback Functions
Next, several callback functions are created that will be called when some event on the mesh happens.
The receivedCallback() function prints the message sender (from) and the content of the message (msg.c_str()).
void receivedCallback( uint32_t from, String &msg ) {
Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str());
The message comes in JSON format, so, we can access the variables as follows:
JSONVar myObject = JSON.parse(msg.c_str());
int node = myObject["node"];
double temp = myObject["temp"];
double hum = myObject["hum"];
double pres = myObject["pres"];
Finally, print all the information on the Serial Monitor.
Serial.print("Node: ");
Serial.println(node);
Serial.print("Temperature: ");
Serial.print(temp);
Serial.println(" C");
Serial.print("Humidity: ");
Serial.print(hum);
Serial.println(" %");
Serial.print("Pressure: ");
Serial.print(pres);
Serial.println(" hpa");
The newConnectionCallback() function runs whenever a new node joins the network. This function simply prints the chip ID of the new node. You can modify the function to do any other task.
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("--> startHere: New Connection, nodeId = %u\n", nodeId);
}
The changedConnectionCallback() function runs whenever a connection changes on the network (when a node joins or leaves the network).
void changedConnectionCallback() {
Serial.printf("Changed connections\n");
}
The nodeTimeAdjustedCallback() function runs when the network adjusts the time, so that all nodes are synchronized. It prints the offset.
void nodeTimeAdjustedCallback(int32_t offset) {
Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset);
}
setup()
In the setup(), initialize the serial monitor.
void setup() {
Serial.begin(115200);
Call the initBME() function to initialize the BME280 sensor.
initBME();
Choose the desired debug message types:
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
Initialize the mesh with the details defined earlier.
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
Assign all the callback functions to their corresponding events.
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
Finally, add the taskSendMessage function to the userScheduler. The scheduler is responsible for handling and running the tasks at the right time.
userScheduler.addTask(taskSendMessage);
Finally, enable the taskSendMessage, so that the program starts sending the messages to the mesh.
taskSendMessage.enable();
To keep the mesh running, add mesh.update() to the loop().
void loop() {
// it will run the user scheduler as well
mesh.update();
}
Demonstration
After uploading the code to all your boards (each board with a different node number), you should see that each board is receiving the other boards' messages.
The following screenshot shows the messages received by node 1. It receives the sensor readings from node 2, 3 and 4.
Wrapping Up
We hope you liked this quick introduction to the ESP-MESH networking protocol. You can take a look at the painlessMesh library for more examples.
We intend to create more tutorials about this subject on a near future. So, write your suggestions on the comments' section.
MQTT Publish BME680 Temperature, Humidity, Pressure, and Gas Readings (Arduino IDE)
Learn how to publish BME680 sensor readings (temperature, humidity, pressure and gas air quality) via MQTT with the ESP32 to any platform that supports MQTT or any MQTT client. As an example, we'll publish sensor readings to Node-RED Dashboard and the ESP32 will be programmed using Arduino IDE.
Recommended reading: What is MQTT and How It Works
Project Overview
The following diagram shows a high-level overview of the project we'll build.
The ESP32 requests sensor readings from the BME680 sensor.
The temperature readings are published in the esp/bme680/temperature topic;
Humidity readings are published in the esp/bme680/humiditytopic;
Pressure readings are published in the esp/bme680/pressure topic;
Gas readings are published in the esp/bme680/gas topic;
Node-RED is subscribed to those topics;
Node-RED receives the sensor readings and then displays them on gauges and text fields;
You can receive the readings in any other platform that supports MQTT and handle the readings as you want.
Prerequisites
Before proceeding with this tutorial, make sure you check the following prerequisites.
Arduino IDE
We'll program the ESP32 using Arduino IDE, so make sure you have the ESP32 add-on installed.
Installing the ESP32 Board in Arduino IDE (Windows, Mac OS X, Linux)
MQTT Broker
To use MQTT, you need a broker. We'll be using Mosquitto broker installed on a Raspberry Pi. Read How to Install Mosquitto Broker on Raspberry Pi.
You can use any other MQTT broker, including a cloud MQTT broker. We'll show you how to do that in the code later on.
If you're not familiar with MQTT make sure you read our introductory tutorial: What is MQTT and How It Works.
MQTT Libraries
To use MQTT with the ESP32 we'll use the Async MQTT Client Library.
Installing the Async MQTT Client Library
Click here to download the Async MQTT client library. You should have a .zip folder in your Downloads folder
Unzip the .zip folder and you should get async-mqtt-client-master folder
Rename your folder from async-mqtt-client-master to async_mqtt_client
Move the async_mqtt_client folder to your Arduino IDE installation libraries folder
Finally, re-open your Arduino IDE
Alternatively, you can go to Sketch > Include Library > Add . ZIP library and select the library you've just downloaded.
Installing the Async TCP Library
To use MQTT with the ESP, you also need the Async TCP library.
Click here to download the Async TCP client library. You should have a .zip folder in your Downloads folder
Unzip the .zip folder and you should get AsyncTCP-master folder
Rename your folder from AsyncTCP-master to AsyncTCP
Move the AsyncTCP folder to your Arduino IDE installation libraries folder
Finally, re-open your Arduino IDE
Alternatively, you can go to Sketch > Include Library > Add . ZIP library and select the library you've just downloaded.
BME680 Sensor Libraries
To get readings from the BME680 sensor module, we'll use the Adafruit_BME680 library. You also need to install the Adafruit_Sensor library. Follow the next steps to install the libraries in your Arduino IDE:
1. Open your Arduino IDE and go to Sketch > Include Library > Manage Libraries. The Library Manager should open.
2. Search for adafruit bme680 on the Search box and install the library.
To use the BME680 library, you also need to install the Adafruit Unified Sensor. Follow the next steps to install the library in your Arduino IDE:
3. Search for Adafruit Unified Sensorin the search box. Scroll all the way down to find the library and install it.
After installing the libraries, restart your Arduino IDE.
To learn more about the BME680 sensor, read our guide: ESP32 with BME680 Sensor using Arduino IDE (Pressure, Temperature, Humidity).
Parts Required
For this tutorial you need the following parts:
ESP32 (read Best ESP32 development boards)
BME680 BME680 with ESP32 Guide
Raspberry Pi board (read Best Raspberry Pi Starter Kits)
MicroSD Card 16GB Class10
Raspberry Pi Power Supply (5V 2.5A)
Jumper wires
Breadboard
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Schematic Diagram
Wire the BME680 to the ESP32 as shown in the following schematic diagram with the SDA pin connected to GPIO 21 and the SCL pin connected to GPIO 22.
Code
Copy the following code to your Arduino IDE. To make it work for you, you need to insert your network credentials as well as the MQTT broker details.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-mqtt-publish-bme680-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
#include <WiFi.h>
extern "C" {
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
}
#include <AsyncMqttClient.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// Raspberry Pi Mosquitto MQTT Broker
#define MQTT_HOST IPAddress(192, 168, 1, XXX)
// For a cloud MQTT broker, type the domain name
//#define MQTT_HOST "example.com"
#define MQTT_PORT 1883
// Temperature MQTT Topics
#define MQTT_PUB_TEMP "esp/bme680/temperature"
#define MQTT_PUB_HUM "esp/bme680/humidity"
#define MQTT_PUB_PRES "esp/bme680/pressure"
#define MQTT_PUB_GAS "esp/bme680/gas"
/*#define BME_SCK 14
#define BME_MISO 12
#define BME_MOSI 13
#define BME_CS 15*/
Adafruit_BME680 bme; // I2C
//Adafruit_BME680 bme(BME_CS); // hardware SPI
//Adafruit_BME680 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK);
// Variables to hold sensor readings
float temperature;
float humidity;
float pressure;
float gasResistance;
AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;
unsigned long previousMillis = 0; // Stores last time temperature was published
const long interval = 10000; // Interval at which to publish sensor readings
void getBME680Readings(){
// Tell BME680 to begin measurement.
unsigned long endTime = bme.beginReading();
if (endTime == 0) {
Serial.println(F("Failed to begin reading :("));
return;
}
if (!bme.endReading()) {
Serial.println(F("Failed to complete reading :("));
return;
}
temperature = bme.temperature;
pressure = bme.pressure / 100.0;
humidity = bme.humidity;
gasResistance = bme.gas_resistance / 1000.0;
}
void connectToWifi() {
Serial.println("Connecting to Wi-Fi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
void connectToMqtt() {
Serial.println("Connecting to MQTT...");
mqttClient.connect();
}
void WiFiEvent(WiFiEvent_t event) {
Serial.printf("[WiFi-event] event: %d\n", event);
switch(event) {
case SYSTEM_EVENT_STA_GOT_IP:
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
connectToMqtt();
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
Serial.println("WiFi lost connection");
xTimerStop(mqttReconnectTimer, 0); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi
xTimerStart(wifiReconnectTimer, 0);
break;
}
}
void onMqttConnect(bool sessionPresent) {
Serial.println("Connected to MQTT.");
Serial.print("Session present: ");
Serial.println(sessionPresent);
}
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
Serial.println("Disconnected from MQTT.");
if (WiFi.isConnected()) {
xTimerStart(mqttReconnectTimer, 0);
}
}
/*void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
Serial.println("Subscribe acknowledged.");
Serial.print(" packetId: ");
Serial.println(packetId);
Serial.print(" qos: ");
Serial.println(qos);
}
void onMqttUnsubscribe(uint16_t packetId) {
Serial.println("Unsubscribe acknowledged.");
Serial.print(" packetId: ");
Serial.println(packetId);
}*/
void onMqttPublish(uint16_t packetId) {
Serial.print("Publish acknowledged.");
Serial.print(" packetId: ");
Serial.println(packetId);
}
void setup() {
Serial.begin(115200);
Serial.println();
if (!bme.begin()) {
Serial.println(F("Could not find a valid BME680 sensor, check wiring!"));
while (1);
}
mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));
WiFi.onEvent(WiFiEvent);
mqttClient.onConnect(onMqttConnect);
mqttClient.onDisconnect(onMqttDisconnect);
//mqttClient.onSubscribe(onMqttSubscribe);
//mqttClient.onUnsubscribe(onMqttUnsubscribe);
mqttClient.onPublish(onMqttPublish);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
// If your broker requires authentication (username and password), set them below
//mqttClient.setCredentials("REPlACE_WITH_YOUR_USER", "REPLACE_WITH_YOUR_PASSWORD");
connectToWifi();
// Set up oversampling and filter initialization
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms
}
void loop() {
unsigned long currentMillis = millis();
// Every X number of seconds (interval = 10 seconds)
// it publishes a new MQTT message
if (currentMillis - previousMillis >= interval) {
// Save the last time a new reading was published
previousMillis = currentMillis;
getBME680Readings();
Serial.println();
Serial.printf("Temperature = %.2f oC \n", temperature);
Serial.printf("Humidity = %.2f % \n", humidity);
Serial.printf("Pressure = %.2f hPa \n", pressure);
Serial.printf("Gas Resistance = %.2f KOhm \n", gasResistance);
// Publish an MQTT message on topic esp/bme680/temperature
uint16_t packetIdPub1 = mqttClient.publish(MQTT_PUB_TEMP, 1, true, String(temperature).c_str());
Serial.printf("Publishing on topic %s at QoS 1, packetId: %i", MQTT_PUB_TEMP, packetIdPub1);
Serial.printf("Message: %.2f \n", temperature);
// Publish an MQTT message on topic esp/bme680/humidity
uint16_t packetIdPub2 = mqttClient.publish(MQTT_PUB_HUM, 1, true, String(humidity).c_str());
Serial.printf("Publishing on topic %s at QoS 1, packetId %i: ", MQTT_PUB_HUM, packetIdPub2);
Serial.printf("Message: %.2f \n", humidity);
// Publish an MQTT message on topic esp/bme680/pressure
uint16_t packetIdPub3 = mqttClient.publish(MQTT_PUB_PRES, 1, true, String(pressure).c_str());
Serial.printf("Publishing on topic %s at QoS 1, packetId %i: ", MQTT_PUB_PRES, packetIdPub3);
Serial.printf("Message: %.2f \n", pressure);
// Publish an MQTT message on topic esp/bme680/gas
uint16_t packetIdPub4 = mqttClient.publish(MQTT_PUB_GAS, 1, true, String(gasResistance).c_str());
Serial.printf("Publishing on topic %s at QoS 1, packetId %i: ", MQTT_PUB_GAS, packetIdPub4);
Serial.printf("Message: %.2f \n", gasResistance);
}
}
View raw code
How the Code Works
The following section imports all the required libraries.
#include <WiFi.h>
extern "C" {
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
}
#include <AsyncMqttClient.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"
Include your network credentials on the following lines.
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
Insert the Raspberry Pi IP address, so that the ESP32 connects to your broker.
#define MQTT_HOST IPAddress(192, 168, 1, 106)
If you're using a cloud MQTT broker, insert the broker domain name, for example:
#define MQTT_HOST "example.com"
Define the MQTT port.
#define MQTT_PORT 1883
The temperature, humidity and pressure will be published on the following topics:
#define MQTT_PUB_TEMP "esp/bme680/temperature"
#define MQTT_PUB_HUM "esp/bme680/humidity"
#define MQTT_PUB_PRES "esp/bme680/pressure"
#define MQTT_PUB_GAS "esp/bme680/gas"
Initialize a Adafruit_BME680 object called bme.
Adafruit_BME680 bme;
The temperature, humidity, pressure, and gasResistance variables will hold all sensor readings from the BME680 sensor.
float temperature;
float humidity;
float pressure;
float gasResistance;
Create an AsyncMqttClient object called mqttClient to handle the MQTT client and timers to reconnect to your MQTT broker and router when it disconnects.
AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;
Then, create some auxiliary timer variables to publish the readings every 10 seconds. You can change the delay time on the interval variable.
unsigned long previousMillis = 0;
const long interval = 10000;
MQTT functions: connect to Wi-Fi, connect to MQTT, and Wi-Fi events
We haven't added any comments to the functions defined in the next code section. Those functions come with the Async Mqtt Client library. The function's names are pretty self-explanatory.
For example, the connectToWifi() connects your ESP32 to your router:
void connectToWifi() {
Serial.println("Connecting to Wi-Fi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
The connectToMqtt() connects your ESP32 to your MQTT broker:
void connectToMqtt() {
Serial.println("Connecting to MQTT");
mqttClient.connect();
}
The WiFiEvent() function is responsible for handling the Wi-Fi events. For example, after a successful connection with the router and MQTT broker, it prints the ESP32 IP address. On the other hand, if the connection is lost, it starts a timer and tries to reconnect.
void WiFiEvent(WiFiEvent_t event) {
Serial.printf("[WiFi-event] event: %d\n", event);
switch(event) {
case SYSTEM_EVENT_STA_GOT_IP:
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
connectToMqtt();
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
Serial.println("WiFi lost connection");
xTimerStop(mqttReconnectTimer, 0);
xTimerStart(wifiReconnectTimer, 0);
break;
}
}
The onMqttConnect() function runs after starting a session with the broker.
void onMqttConnect(bool sessionPresent) {
Serial.println("Connected to MQTT.");
Serial.print("Session present: ");
Serial.println(sessionPresent);
}
MQTT functions: disconnect and publish
If the ESP32 loses connection with the MQTT broker, it calls the onMqttDisconnect function that prints that message in the serial monitor.
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
Serial.println("Disconnected from MQTT.");
if (WiFi.isConnected()) {
xTimerStart(mqttReconnectTimer, 0);
}
}
When you publish a message to an MQTT topic, the onMqttPublish() function is called. It prints the packet id in the Serial Monitor.
void onMqttPublish(uint16_t packetId) {
Serial.println("Publish acknowledged.");
Serial.print(" packetId: ");
Serial.println(packetId);
}
Basically, all these functions that we've just mentioned are callback functions. So, they are executed asynchronously.
setup()
Now, let's proceed to the setup(). Initialize the BME680 sensor.
if (!bme.begin()) {
Serial.println(F("Could not find a valid BME680 sensor, check wiring!"));
while (1);
}
The next two lines create timers that will allow both the MQTT broker and Wi-Fi connection to reconnect, in case the connection is lost.
mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));
The following line assigns a callback function, so when the ESP32 connects to your Wi-Fi, it will execute the WiFiEvent() function to print the details described earlier.
WiFi.onEvent(WiFiEvent);
Finally, assign all the callbacks functions. This means that these functions will be executed automatically when needed. For example, when the ESP32 connects to the broker, it automatically calls the onMqttConnect() function, and so on.
mqttClient.onConnect(onMqttConnect);
mqttClient.onDisconnect(onMqttDisconnect);
//mqttClient.onSubscribe(onMqttSubscribe);
//mqttClient.onUnsubscribe(onMqttUnsubscribe);
mqttClient.onPublish(onMqttPublish);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
Broker Authentication
If your broker requires authentication, uncomment the following line and insert your credentials (username and password).
mqttClient.setCredentials("REPlACE_WITH_YOUR_USER", "REPLACE_WITH_YOUR_PASSWORD");
Connect to Wi-Fi.
connectToWifi();
Finally, set up the following parameters (oversampling, filter and gas heater) for the sensor.
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150);
loop()
In the loop(), you create a timer that allows you to get new readings from the BME680 sensor and publishing them on the corresponding topic every 10 seconds.
unsigned long currentMillis = millis();
// Every X number of seconds (interval = 10 seconds)
// it publishes a new MQTT message
if (currentMillis - previousMillis >= interval) {
// Save the last time a new reading was published
previousMillis = currentMillis;
getBME680Readings();
Serial.println();
Serial.printf("Temperature = %.2f oC \n", temperature);
Serial.printf("Humidity = %.2f % \n", humidity);
Serial.printf("Pressure = %.2f hPa \n", pressure);
Serial.printf("Gas Resistance = %.2f KOhm \n", gasResistance);
Learn more about getting readings from the BME680 sensor: ESP32 with BME680 Temperature, Humidity and Pressure Sensor Guide.
Publishing to topics
To publish the readings on the corresponding MQTT topics, use the next lines:
uint16_t packetIdPub1 = mqttClient.publish(MQTT_PUB_TEMP, 1, true, String(temperature).c_str());
uint16_t packetIdPub2 = mqttClient.publish(MQTT_PUB_HUM, 1, true, String(humidity).c_str());
uint16_t packetIdPub3 = mqttClient.publish(MQTT_PUB_PRES, 1, true, String(pressure).c_str());
uint16_t packetIdPub4 = mqttClient.publish(MQTT_PUB_GAS, 1, true, String(gasResistance).c_str());
Basically, use the publish() method on the mqttClient object to publish data on a topic. The publish() method accepts the following arguments, in order:
MQTT topic (const char*)
QoS (uint8_t): quality of service it can be 0, 1 or 2
retain flag (bool): retain flag
payload (const char*) in this case, the payload corresponds to the sensor reading
The QoS (quality of service) is a way to guarantee that the message is delivered. It can be one of the following levels:
0: the message will be delivered once or not at all. The message is not acknowledged. There is no possibility of duplicated messages;
1: the message will be delivered at least once, but may be delivered more than once;
2: the message is always delivered exactly once;
Learn about MQTT QoS.
Uploading the code
With your Raspberry Pi powered on and running the Mosquitto MQTT broker, upload the code to your ESP32.
Open the Serial Monitor at a baud rate of 115200 and you'll see that the ESP32 starts publishing messages on the topics we've defined previously.
Preparing Node-RED Dashboard
The ESP32 is publishing sensor readings every 10 seconds on four MQTT topics. Now, you can use any dashboard that supports MQTT or any other device that supports MQTT to subscribe to those topics and receive the readings.
As an example, we'll create a simple flow using Node-RED to subscribe to those topics and display the readings on gauges.
If you don't have Node-RED installed, follow the next tutorials:
Getting Started with Node-RED on Raspberry Pi
Installing and Getting Started with Node-RED Dashboard
Having Node-RED running on your Raspberry Pi, go to your Raspberry Pi IP address followed by :1880.
http://raspberry-pi-ip-address:1880
The Node-RED interface should open. Drag four MQTT in nodes, two gauge nodes, and two text field nodes to the flow.
Click the MQTT node and edit its properties.
The Server field refers to the MQTT broker. In our case, the MQTT broker is the Raspberry Pi, so it is set to localhost:1883. If you're using a Cloud MQTT broker, you should change that field.
Insert the topic you want to be subscribed to and the QoS. This previous MQTT node is subscribed to the esp/bme680/temperature topic.
Click on the other MQTT in nodes and edit its properties with the same server, but for the other topics: esp/bme680/humidity, esp/bme680/pressure, and esp/bme680/gas.
Click on the gauge nodes and edit its properties for each reading. The following node is set for the temperature readings. Edit the other chart nodes for the humidity readings.
Wire your nodes as shown below:
Finally, deploy your flow (press the button on the upper right corner).
Alternatively, you can go to Menu > Import and copy the following to your Clipboard to create your Node-RED flow.
[{"id":"3b7f947c.9759ec","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/temperature","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":470,"y":2640,"wires":[["b87b21c3.96672"]]},{"id":"b87b21c3.96672","type":"ui_gauge","z":"254c9c97.f85b34","name":"","group":"37de8fe8.46846","order":2,"width":0,"height":0,"gtype":"gage","title":"Temperature","label":"oC","format":"{{value}}","min":0,"max":"40","colors":["#00b500","#f7df09","#ca3838"],"seg1":"","seg2":"","x":690,"y":2640,"wires":[]},{"id":"f92248f4.545778","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/humidity","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":460,"y":2700,"wires":[["4114a401.5ac69c"]]},{"id":"4114a401.5ac69c","type":"ui_gauge","z":"254c9c97.f85b34","name":"","group":"37de8fe8.46846","order":2,"width":0,"height":0,"gtype":"gage","title":"Humidity","label":"%","format":"{{value}}","min":"30","max":"100","colors":["#53a4e6","#1d78a9","#4e38c9"],"seg1":"","seg2":"","x":680,"y":2700,"wires":[]},{"id":"ad51f895.2c2848","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/pressure","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":460,"y":2760,"wires":[["3a95123b.66405e"]]},{"id":"c074e688.198b78","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/gas","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":440,"y":2820,"wires":[["d3539c06.00a17"]]},{"id":"3a95123b.66405e","type":"ui_text","z":"254c9c97.f85b34","group":"37de8fe8.46846","order":2,"width":0,"height":0,"name":"","label":"Pressure","format":"{{msg.payload}} hPa","layout":"row-spread","x":680,"y":2760,"wires":[]},{"id":"d3539c06.00a17","type":"ui_text","z":"254c9c97.f85b34","group":"37de8fe8.46846","order":3,"width":0,"height":0,"name":"","label":"Gas","format":"{{msg.payload}} KOhm","layout":"row-spread","x":670,"y":2820,"wires":[]},{"id":"8db3fac0.99dd48","type":"mqtt-broker","z":"","name":"","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"37de8fe8.46846","type":"ui_group","z":"","name":"BME680","tab":"53b8c8f9.cfbe48","order":1,"disp":true,"width":"6","collapse":false},{"id":"53b8c8f9.cfbe48","type":"ui_tab","z":"","name":"Home","icon":"dashboard","order":5,"disabled":false,"hidden":false}]
View raw code
Demonstration
Go to your Raspberry Pi IP address followed by :1880/ui.
http://raspberry-pi-ip-address:1880/ui
You should get access to the current BME680 sensor readings on the Dashboard. You can use other dashboard-type nodes to display the readings on different ways.
That's it! You have your ESP32 board publishing BME680 temperature, humidity, pressure and gas resistance readings to Node-RED via MQTT.
Wrapping Up
MQTT is a great communication protocol to exchange small amounts of data between devices. In this tutorial you've learned how to publish temperature, humidity, pressure and gas resistance readings from a BME680 environmental sensor with the ESP32 to different MQTT topics. Then, you can use any device or home automation platform to subscribe to those topics and receive the readings.
Instead of a BME680 sensor, you can use any other sensor like a DS18B20 temperature sensor, a DHT22 temperature and humidity sensor or a BME280 temperature, humidity and pressure sensor:
ESP32 MQTT Publish DS18B20 Temperature Readings
ESP32 MQTT Publish DHT22/DHT11 Sensor Readings
ESP32 MQTT Publish BME280 Sensor Readings
We hope you've found this tutorial useful. If you want to learn more about the ESP32, take a look at our resources:
Learn ESP32 with Arduino IDE
MicroPython Programming with ESP32 and ESP8266
More ESP32 Projects and Tutorials
Thanks for reading.
Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD
Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD