Introduction: Low Cost Automation ESP32 and 16 Relays

About: Do you like technology? Follow my channel on Youtube and my Blog. In them I put videos every week of microcontrollers, arduinos, networks, among other subjects.

Today, we’ll discuss an automation project with ESP32 as a Web Server! We’ll use ESP32 to display a page with temperature, humidity, and buttons to change the state of relays. We’ll use the MCP23017 to increase the number of pins available in the ESP, which allows us to connect up to 128 relays.

I want to point out that in this video you will realize that it is not worth doing a project using the ESP-01 with Arduino Mega, as previously shown in the video, “Arduino Mega + WiFi = Automation.” I had done that specific video at the request of several followers. In my opinion, the ESP32 is much better, due to the total control that this microcontroller gives you.

Step 1: ESP32 Pinout

Step 2: Assembly

In this image you can notice that I only used two pins to connect 16 relays, which is made possible by MCP23017. You can see about this in the video, “IO Expander for ESP32, ESP8266 and Arduino.” This specific example is with the MCP23016 version, which uses the same Lib. I also feature the good old DHT22 in our project today.

Step 3: Demonstration

In the image, you see our assembly and a cell phone connected to our local IP-based application, with relay and ESP32 boards being powered by USB.

Through the application, we perform the temperature and humidity reading with the data captured by the DHT22.

Step 4: SimpleDHT Library

In the Arduino IDE, go to Sketch-> Include Library-> Manage Libraries ...

Install SimpleDHT

Step 5: WebServer Library

Go to C: \ Users \ <YOUR_USER> \ Documents \ Arduino \ hardware \ espressif \ esp32 \ libraries and make sure you have the WebServer folder. You do not have to reinstall the core of ESP32 in the Arduino IDE, since newer versions already come with the lib.

Step 6: ESP32_WebServer.ino

We will include the required libraries and set the i2c address of the MCP23017, as well as the registers.

#include <WiFi.h>
#include <WiFiCliente.h> #include <WebServer.h> #include <SimpleDHT.H> #include <Wire.h> #include <FS.h> #include <SPIFFS.h> //endereço I2C do MCP23017 #define MCP_ADDRESS 0x20 //ENDEREÇOS DE REGISTRADORES #define GPA 0x12 // DATA PORT REGISTER A #define GPB 0x13 // DATA PORT REGISTER B #define IODIRA 0x00 // I/O DIRECTION REGISTER A #define IODIRB 0x01 // I/O DIRECTION REGISTER B #define PINS_COUNT 16 //Quantidade total de pinos #define DATA_PATH "/pin_data.bin" //Arquivo onde serão salvos os status dos pinos #define DHTPIN 5 //Pino one está o DHT22

Here, we have the variables that involve ssid, password, and IP. We create a server on the default port and point to the object reading temperature and humidity. We also have the variables to store these read values. We continue to save the status of the two ports of the MCP23017 and execute the watchdog timer control (watch watchdog video: Catch! And now?).

const char *ssid = "TesteESP";
const char *password = "12345678"; const char *ip = ""; //Criamos um server na porta padrão o http WebServer server(80); //Objeto que faz a leitura da temperatura e umidade SimpleDHT22 dht; //Variáveis para guardar os valores de temperatura e umidade lidos float temperature = 0; float humidity = 0; //Guarda o estado atual das duas portas do MCP23017 (8 bits cada) uint8_t currentValueGPA = 0; uint8_t currentValueGPB = 0; //faz o controle do temporizador do watchdog (interrupção por tempo) hw_timer_t *timer = NULL;

Step 7: ESP32_WebServer.ino - Setup

We initialize the MCP23017 pin values, and also initialize SPIFFS and WiFi. We define that whenever we receive a request in the webserver root, the handleRoot function will be executed. We also initialize the server and the watchdog.

void setup()
{ Serial.begin(115200); //Inicializa os valores dos pinos do MCP23017 setupPins(); //Tenta inicializar SPIFFS if(SPIFFS.begin(true)) { loadPinStatus(); } else { //Se não conseguiu inicializar Serial.println("SPIFFS Mount Failed"); } //inicializa WiFi setupWiFi(); //Sempre que recebermos uma requisição na raiz do webserver //a função handleRoot será executada server.on("/", handleRoot); //Se recebermos uma requisição em uma rota que nao existe server.onNotFound(handleNotFound); //Inicializa o server server.begin(); //Inicializa o watchdog setupWatchdog(); }

Step 8: ESP32_WebServer.ino - SetupPins

We initialize the Wire in the standard SDA and SCL pins of the ESP32 and point to the communication speed. We then configure all the pins of the two ports of the MCP23017 as outputs.

void setupPins()
{ //Inicializa o Wire nos pinos SDA e SCL padrões do ESP32 Wire.begin(); //Velocidade de comunicação Wire.setClock(200000); //Configura todos os pinos das duas portas do MCP23017 como saída configurePort(IODIRA, OUTPUT); configurePort(IODIRB, OUTPUT); }

Step 9: ESP32_WebServer.ino - ReadDHT

Here, we have a function to recover the current state of the pins in the file so that they remain after a possible reboot.

//Função para recuperar o estado atual dos pinos no
//arquivo para que estes permaneçam após um eventual reboot void loadPinStatus() { //Abre o arquivo para leitura File file =, FILE_READ); //Se arquivo não existe if(!file) { //Na primeira vez o arquivo ainda não foi criado Serial.println("Failed to open file for reading"); //Coloca todos os pinos das duas portas do MCP23017 em LOW writeBlockData(GPA, B00000000); writeBlockData(GPB, B00000000); return; } //Faz a leitura dos valores¤tValueGPA, 1);¤tValueGPB, 1); //fecha o arquivo file.close(); //Envia os valores para o MCP23017 writeBlockData(GPA, currentValueGPA); writeBlockData(GPB, currentValueGPB); }

Step 10: ESP32_WebServer.ino - SetupWiFi

In this step, we put it into station mode and connect to the network. We set the IP and display this address to open in the browser.

void setupWiFi()
{ //Coloca como modo station WiFi.mode(WIFI_STA); //Conecta à rede WiFi.begin(ssid, password); Serial.println(""); //Enquanto não conectar while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } //Se chegou aqui está conectado Serial.println(""); Serial.print("Connected to "); Serial.println(ssid); //Configura o IP IPAddress ipAddress; ipAddress.fromString(ip); WiFi.config(ipAddress, WiFi.gatewayIP(), WiFi.subnetMask()); //Exibe o endereço de IP para abrir no navegador Serial.print("IP address: "); Serial.println(WiFi.localIP()); }

Step 11: ESP32_WebServer.ino - WatchDog

Here, we have the function that the timer will call to restart the ESP32, as well as the function that sets the timer.

//função que o temporizador irá chamar, para reiniciar o ESP32
void IRAM_ATTR resetModule(){ ets_printf("(watchdog) reboot\n"); esp_restart_noos(); //reinicia o chip } //função que configura o temporizador void setupWatchdog() { timer = timerBegin(0, 80, true); //timerID 0, div 80 //timer, callback, interrupção de borda timerAttachInterrupt(timer, &resetModule, true); //timer, tempo (us), repetição timerAlarmWrite(timer, 5000000, true); timerAlarmEnable(timer); //habilita a interrupção //enable interrupt }

Step 12: ESP32_WebServer.ino - Loop

In the Loop, we have the function that resets the timer (feeds the watchdog), and the one that checks if there is any request.

void loop()
{ //reseta o temporizador (alimenta o watchdog) timerWrite(timer, 0); //Verifica se existe alguma requisição server.handleClient(); }

Step 13: ESP32_WebServer.ino - HandleRoot

At this stage, we verify that the assembly has received arguments in the request, and then we execute an action. We read the temperature and humidity, generate the html, and send it. Finally, we saved the status of the pins to return in this specific format in the next reboot.

void handleRoot()
{ //Se recebeu argumentos na requisição if(server.args() > 0) { //Executa a ação (on ou off) no pino do argumento execute(server.argName(0), server.arg(0)); } //Faz a leitura da temperatura e umidade readDHT(); //Gera o html e o envia String html = " "; html.concat(head()); html.concat(body()); html.concat("

"); server.send(200, "text/html; charset=UTF-8", html); //Salva o status dos pinos para voltar assim no próximo reboot savePinStatus(); }

Step 14: ESP32_WebServer.ino - SavePinStatus

Here, we work with the function to save the current state of the pins in the file so that it remains after a possible reboot.

//Função para salvar o estado atual dos pinos em
//arquivo para que estes permaneçam após um eventual reboot void savePinStatus() { //Abre o arquivo para escrita File file =, FILE_WRITE); //Se não conseguiu abrir/criar o arquivo if(!file) { Serial.println("Failed to open file for writing"); return; } //Escreve os valores dos pinos no começo do arquivo; file.write(¤tValueGPA, 1); file.write(¤tValueGPB, 1); //Fecha o arquivo file.close(); }

Step 15: ESP32_WebServer.ino - HandleNotFound

This function is used to send information to the navigator to communicate that the route was not found.

void handleNotFound()
{ //Envia para o navegador a informação que a rota não foi encontrada server.send(404, "text/plain", "Not Found"); }

Step 16: ESP32_WebServer.ino - Execute

It executes the action next to the value (relay number).

//Executada a ação junto ao valor (número do relê)
void execute(String action, String value) { //Se é uma das duas ações que esperamos if(action == "on" || action == "off") { //Os relês são numerados a partir do 1, mas o array começa do 0 //então tiramos 1 int index = value.toInt() - 1; int status = action == "on" ? HIGH : LOW; digitalWriteMCP(index, status); } }

Step 17: ESP32_WebServer.ino - Head

We return the page header with the time information to refresh both the page by itself and the appearance.

//Retorna o cabeçalho da página com a informação do tempo
//para atualizar a página sozinho e a aparência String head() { return (F("<head>" "<meta name='viewport' content='width=device-width, initial-scale=1.0'>"

"<meta http-equiv='refresh' content='10;URL=/'>" //refresh a cada 10 segundos

"<style> "         "body{"             "text-align: center;"             "font-family: sans-serif;"             "font-size: 14px;"         "}"         "p{"             "color:#555;"             "font-size: 12px;"         "}"         ".button{"             "outline: none;"             "display: block;"             "border: 1px solid #555;"             "border-radius:18px;"             "width: 150px;"             "height: 30px;"             "margin: 10px;"             "margin-left: auto;"             "margin-right: auto;"             "cursor: pointer;"         "}"         ".button_off{"             "background-color:#FFF;"             "color: #555;"         "}"         ".button_on{"             "background-color:#2C5;"             "color: #fff;"         "}"     "

"text-align: center;" "font-family: sans-serif;" "font-size: 14px;" "}" "p{" "color:#555;" "font-size: 12px;" "}" ".button{" "outline: none;" "display: block;" "border: 1px solid #555;" "border-radius:18px;" "width: 150px;" "height: 30px;" "margin: 10px;" "margin-left: auto;" "margin-right: auto;" "cursor: pointer;" "}" ".button_off{" "background-color:#FFF;" "color: #555;" "}" ".button_on{" "background-color:#2C5;" "color: #fff;" "}" "</style>"



"         "body{"             "text-align: center;"             "font-family: sans-serif;"             "font-size: 14px;"         "}"         "p{"             "color:#555;"             "font-size: 12px;"         "}"         ".button{"             "outline: none;"             "display: block;"             "border: 1px solid #555;"             "border-radius:18px;"             "width: 150px;"             "height: 30px;"             "margin: 10px;"             "margin-left: auto;"             "margin-right: auto;"             "cursor: pointer;"         "}"         ".button_off{"             "background-color:#FFF;"             "color: #555;"         "}"         ".button_on{"             "background-color:#2C5;"             "color: #fff;"         "}"     "

Step 18: ESP32_WebServer.ino - Body

Now, this function displays the sensor data and creates the buttons, one for each pin that has a relay.

//Exibe os dados dos sensores e cria os botões
String body() { String b = "<body>" "<p>Temperature: " + String(temperature) + " °C</p>"

"<p>Humidity: " + String(humidity) + "%</p>";

//Cria um botão para cada pino que possui um relê for(int i=0; i<PINS_COUNT;i++)

b.concat(button(i)); } b.concat("</body>");

return b; }

Step 19: ESP32_WebServer.ino - Button

Here, we have the creation of a button with the appearance and action that corresponds to the current state of the relay.

//Cria um botão com a aparência e ação correspondente ao estado atual do relê
String button(int number) { String label = String(number + 1); String className = "button "; className += getPinStatus(number) == HIGH ? "button_on" : "button_off"; String action = getPinStatus(number) == HIGH ? "off" : "on"; return "<button class=\"" + className + "\"onclick=\"location.href='?" + action + "=" + label + "'\">" + label + "</button>";


Step 20: ESP32_WebServer.ino - GetPinStatus

We check if the relay is on or off.

uint8_t getPinStatus(int pin)
{ uint8_t v; //de 0 a 7 porta A, de 8 a 15 porta B if(pin < 8) { v = currentValueGPA; } else { v = currentValueGPB; pin -= 8; } return !!(v & (1 << pin));

Step 21: ESP32_WebServer.ino - ConfigurePort

Here, we configure the mode of the port pins (GPA or GPB). As a parameter, we pass:

port: GPA or GPB

type: INPUT for all port pins to work as input

OUTPUT for all port pins to work as output

//Configura o modo dos pinos das portas (GPA ou GPB)
//como parametro passamos: // port: GPA ou GPB // type: // INPUT para todos os pinos da porta trabalharem como entrada // OUTPUT para todos os pinos da porta trabalharem como saída void configurePort(uint8_t port, uint8_t type) { if(type == INPUT) { writeBlockData(port, 0xFF); } else if(type == OUTPUT) { writeBlockData(port, 0x00); } }

Step 22: ESP32_WebServer.ino - DigitalWriteMCP

We change the state of a desired pin (0 to 15), save the values of the corresponding port bits, and send the data to p MCP.

//muda o estado de um pino desejado
void digitalWriteMCP(int pin, int value) { uint8_t port; uint8_t v; //de 0 a 7 porta A, de 8 a 15 porta B if(pin < 8){ port = GPA; v = currentValueGPA; }else{ port = GPB; v = currentValueGPB; pin -= 8; } if (value == LOW){ v &= ~(B00000001 << (pin)); // muda o pino para LOW } else if (value == HIGH){ v |= (B00000001 << (pin)); // muda o pino para HIGH } //Salva os valores dos bits da porta correspondente if(port == GPA){ currentValueGPA = v; }else{ currentValueGPB = v; } //envia os dados para o MCP writeBlockData(port, v); }

Step 23: ESP32_WebServer.ino - WriteBlockData

We then send the data to the MCP23017 via the i2c bus.

//envia dados para o MCP23017 através do barramento i2c
void writeBlockData(uint8_t port, uint8_t data) { Wire.beginTransmission(MCP_ADDRESS); Wire.write(port); Wire.write(data); Wire.endTransmission(); delay(10); }

Step 24: ESP32_WebServer.ino - ReadDHT

We read the temperature and humidity.

//Faz a leitura da temperatura e umidade
void readDHT() { float t, h; int status = dht.read2(DHTPIN, &t, &h, NULL); //Apenas altera as variáveis se a leitura foi bem sucedida if (status == SimpleDHTErrSuccess) { temperature = t; humidity = h; } }

Step 25: Files

Download the files: