ESP32: How to create a Keypad Interface and Web Server

ESP32 Tutorial: Keypad Interface and Web Server

Abstract

This tutorial demonstrates how to interface a standard 4×3 Matrix Keypad with the ESP32 and display the pressed digits on a web page hosted by the ESP32’s built-in web server. This requires reading the keypad matrix, accumulating the digits pressed, and using the web server to serve an HTML page that dynamically updates the displayed number via a simple refresh mechanism. The special keys (∗ and #) are used to clear the displayed number.

1. Introduction: Keypad Interface and Web Server

This tutorial guides you through an essential Intermediate ESP32 project that bridges physical input with a modern web interface. The goal is to interface a standard 4×3 Matrix Keypad with the ESP32 and display the entered digits in real-time on a web page hosted directly by the microcontroller. This setup combines two core skills: managing external hardware communication (keypad matrix scanning) and utilizing the ESP32’s capability to act as a standalone Web Server using the Arduino framework.

You’ll learn how to configure the Keypad library with flexible ESP32 GPIO pins to read matrix inputs reliably. The pressed digits will be collected in a global string variable. This string is then dynamically injected into the HTML content, allowing any connected web browser to display the input instantly. This project is a practical foundation for building access control systems, connected interfaces, or remote configuration tools.

2. Prerequisites and Wiring

2.1 Hardware and Library

  • Hardware: ESP32 Dev Board, 4×3 Matrix Keypad.
  • Library: You must install the Keypad Library via the Arduino Library Manager.

2.2 Keypad-to-ESP32 Wiring

A 4×3 keypad has 7 wires (4 rows, 3 columns). These must be connected to 7 available ESP32 GPIO pins.

Keypad Pin

Function

ESP32 GPIO Pin

Notes

R1

Row 1

GPIO 13

Output pins (rows) should be able to drive current.

R2

Row 2

GPIO 12

 

R3

Row 3

GPIO 14

 

R4

Row 4

GPIO 27

 

C1

Column 1

GPIO 26

Input pins (columns) will use internal pull-downs.

C2

Column 2

GPIO 25

 

C3

Column 3

GPIO 33

 

3. Web Server Code and Logic

The core logic uses the ESP32 to track the keyed digits in a string variable and serve the HTML page, passing the string value.

3.1 Global Variables and Keypad Setup

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

// --- Network Credentials ---
const char* ssid = "SSID";
const char* password = "PASSWORD";

WebServer server(80);

// Global variable to hold the digits pressed
String currentNumber = "";

// --- Keypad Setup (4x3 Configuration) ---
const byte ROWS = 4; // four rows
const byte COLS = 3; // three columns

char keys[ROWS][COLS] = {
  {'1', '2', '3'},
  {'4', '5', '6'},
  {'7', '8', '9'},
  {'*', '0', '#'}
};

// Row pins (R1, R2, R3, R4)
byte rowPins[ROWS] = {13, 12, 14, 27};
// Column pins (C1, C2, C3)
byte colPins[COLS] = {26, 25, 33};

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

				
			

3.2 Keypad and Web Handler Functions

The handleKeypad() function is a non-blocking check for key presses, updating the global currentNumber variable.

				
					// Non-blocking function to check the keypad for presses
void handleKeypad() {
  char key = keypad.getKey();  // Read the key

  // Print if key pressed
  if (key) {
    Serial.print("Key Pressed : ");
    Serial.println(key);
  }
  if (key) {
    // If a digit (0-9) is pressed, append it to the number string
    if (key >= '0' && key <= '9') {
      currentNumber += key;
    }
    // If a special key is pressed, reset the number
    else if (key == '*' || key == '#') {
      currentNumber = ""; // Reset the number string
    }
   
    // Send the updated page instantly on any key press
    server.send(200, "text/html", getHtmlPage());
  }
}

// Handler for the root web page (/)
void handleRoot() {
  server.send(200, "text/html", getHtmlPage());
}

				
			

3.3 The Setup and Loop

				
					// --- Setup and Loop ---

void setup() {
  Serial.begin(115200);

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected.");
  Serial.print("Web Server IP: ");
  Serial.println(WiFi.localIP());

  // Setup Web Server Routes
  server.on("/", handleRoot);
  server.begin();
}

void loop() {
  // Must continuously handle incoming web requests
  server.handleClient();
 
  // Must continuously check the keypad for presses
  handleKeypad();
}

				
			

4. HTML Generation

The HTML code includes minimal CSS for a clean look and uses a placeholder KEY_VALUE_PLACEHOLDER that is dynamically replaced by the current number string before being sent to the client.

				
					// --- HTML Generation Function with Custom Styles ---
String getHtmlPage() {
  // Brand Colors Used:
  // #121212 (Dark Background)
  // #00f5a0 (Primary Accent/Green)
  // #d1d5db (Light Gray Text)
  String html = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="refresh" content="1">
  <style>
    body {
      font-family: Arial, sans-serif;
      text-align: center;
      margin-top: 30px;
      background-color: #121212; /* Dark Background */
      color: #d1d5db; /* Light Gray Text */
    }
    .container {
      background-color: #121212;
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.5); /* Enhanced shadow */
      display: inline-block;
    }
    .logo {
      margin-bottom: 20px;
    }
    .logo img {
      max-width: 250px; /* Logo Size */
      height: auto;
    }
    h1 {
      color: #00f5a0; /* Primary Green Accent */
      margin-top: 10px;
    }
    p {
        color: #d1d5db;
    }
    .display {
      font-size: 3em;
      color: #121212; /* Dark text for high contrast on green */
      border: 2px solid #00f5a0;
      padding: 15px 30px;
      margin: 20px 0;
      border-radius: 5px;
      min-width: 250px;
      display: inline-block;
      background-color: #00f5a0; /* Primary Green Background */
      font-weight: bold;
    }
    .instruction {
        color: #00f5a0; /* Primary Green Accent */
    }
  </style>
  <title>ESP32 Keypad Input</title>
</head>
<body>
  <div class="container">
    <div class="logo">
      <img decoding="async" src="https://raw.githubusercontent.com/hackerembedded/STM32/main/Logo%20Hacker%20Embedded.png" alt="Hacker Embedded Logo">
    </div>
    <h1>Keypad Digit Input</h1>
    <p>The number is dynamically updated when a key is pressed.</p>
    <div class="display">KEY_VALUE_PLACEHOLDER</div>
    <p class="instruction">Press * or # on the keypad to clear the display.</p>
  </div>
</body>
</html>
)rawliteral";

  // Replace the placeholder with the actual number string
  html.replace("KEY_VALUE_PLACEHOLDER", currentNumber.isEmpty() ? "---" : currentNumber);
 
  return html;
}

				
			

4.1 Source Code

The entire code can be copied from here:

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

// --- Network Credentials ---
const char* ssid = "SSID";
const char* password = "PASSWORD";

WebServer server(80);

// Global variable to hold the digits pressed
String currentNumber = "";

// --- Keypad Setup (4x3 Configuration) ---
const byte ROWS = 4; // four rows
const byte COLS = 3; // three columns

char keys[ROWS][COLS] = {
  {'1', '2', '3'},
  {'4', '5', '6'},
  {'7', '8', '9'},
  {'*', '0', '#'}
};

// Row pins (R1, R2, R3, R4)
byte rowPins[ROWS] = {13, 12, 14, 27};
// Column pins (C1, C2, C3)
byte colPins[COLS] = {26, 25, 33};

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// --- HTML Generation Function with Custom Styles ---
String getHtmlPage() {
  // Brand Colors Used:
  // #121212 (Dark Background)
  // #00f5a0 (Primary Accent/Green)
  // #d1d5db (Light Gray Text)
  String html = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="refresh" content="1">
  <style>
    body {
      font-family: Arial, sans-serif;
      text-align: center;
      margin-top: 30px;
      background-color: #121212; /* Dark Background */
      color: #d1d5db; /* Light Gray Text */
    }
    .container {
      background-color: #121212;
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.5); /* Enhanced shadow */
      display: inline-block;
    }
    .logo {
      margin-bottom: 20px;
    }
    .logo img {
      max-width: 250px; /* Logo Size */
      height: auto;
    }
    h1 {
      color: #00f5a0; /* Primary Green Accent */
      margin-top: 10px;
    }
    p {
        color: #d1d5db;
    }
    .display {
      font-size: 3em;
      color: #121212; /* Dark text for high contrast on green */
      border: 2px solid #00f5a0;
      padding: 15px 30px;
      margin: 20px 0;
      border-radius: 5px;
      min-width: 250px;
      display: inline-block;
      background-color: #00f5a0; /* Primary Green Background */
      font-weight: bold;
    }
    .instruction {
        color: #00f5a0; /* Primary Green Accent */
    }
  </style>
  <title>ESP32 Keypad Input</title>
</head>
<body>
  <div class="container">
    <div class="logo">
      <img decoding="async" src="https://raw.githubusercontent.com/hackerembedded/STM32/main/Logo%20Hacker%20Embedded.png" alt="Hacker Embedded Logo">
    </div>
    <h1>Keypad Digit Input</h1>
    <p>The number is dynamically updated when a key is pressed.</p>
    <div class="display">KEY_VALUE_PLACEHOLDER</div>
    <p class="instruction">Press * or # on the keypad to clear the display.</p>
  </div>
</body>
</html>
)rawliteral";

  // Replace the placeholder with the actual number string
  html.replace("KEY_VALUE_PLACEHOLDER", currentNumber.isEmpty() ? "---" : currentNumber);
 
  return html;
}


// --- Web Server Handler Functions ---

// Non-blocking function to check the keypad for presses
void handleKeypad() {
  char key = keypad.getKey();  // Read the key

  // Print if key pressed
  if (key) {
    Serial.print("Key Pressed : ");
    Serial.println(key);
  }
  if (key) {
    // If a digit (0-9) is pressed, append it to the number string
    if (key >= '0' && key <= '9') {
      currentNumber += key;
    }
    // If a special key is pressed, reset the number
    else if (key == '*' || key == '#') {
      currentNumber = ""; // Reset the number string
    }
   
    // Send the updated page instantly on any key press
    server.send(200, "text/html", getHtmlPage());
  }
}

// Handler for the root web page (/)
void handleRoot() {
  server.send(200, "text/html", getHtmlPage());
}


// --- Setup and Loop ---

void setup() {
  Serial.begin(115200);

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected.");
  Serial.print("Web Server IP: ");
  Serial.println(WiFi.localIP());

  // Setup Web Server Routes
  server.on("/", handleRoot);
  server.begin();
}

void loop() {
  // Must continuously handle incoming web requests
  server.handleClient();
 
  // Must continuously check the keypad for presses
  handleKeypad();
}


				
			

4.2 Code Explanation: Keypad Web Server

This code establishes an ESP32 web server that simultaneously monitors a 4×3 matrix keypad and displays the entered sequence on a web page. The core mechanism relies on non-blocking code execution to handle both tasks continuously.

4.3 Key Setup and Initialization

  1. Libraries: The sketch uses three main libraries:

    • WiFi.h: For connecting the ESP32 to a local network.

    • WebServer.h: For creating and managing the HTTP server on port 80.

    • Keypad.h: For handling the complex row/column scanning logic of the 4×3 matrix keypad.

  2. Keypad Configuration: The keys 2D array maps the physical layout of the keypad, while rowPins and colPins define the 7 specific ESP32 GPIO pins used for the interface. The Keypad object is initialized with this map.

  3. Global Variable: String currentNumber = ""; stores the accumulated digits. This variable is the bridge between the physical input and the web output.

  4. Networking: In setup(), the ESP32 connects to the specified Wi-Fi network. It then sets up the web server route (server.on("/", handleRoot)) and starts the server (server.begin()).

4.4 Non-Blocking Keypad Logic (handleKeypad)

The handleKeypad() function is called repeatedly in the loop() to check the physical input without pausing the entire program:

  1. Key Read: char key = keypad.getKey(); non-blockingly checks if a key has been pressed since the last check.

  2. Digit Accumulation: If a key is detected (if (key)):

    • Digits (0 through 9) are appended to the global currentNumber string.

    • Special characters (* or #) cause the currentNumber string to be reset ("").

  3. Instant Update: Crucially, if any key is pressed, the code calls server.send(200, "text/html", getHtmlPage());. This immediately sends the updated HTML page back to the browser, providing a near real-time update of the displayed number.

4.5 Web Server and HTML Generation

  1. HTML Structure (getHtmlPage): This function generates the complete HTML page, including styling. The content for the keypad display uses a placeholder (KEY_VALUE_PLACEHOLDER).

  2. Dynamic Content Injection: Before returning the HTML string, the code performs the replacement: html.replace("KEY_VALUE_PLACEHOLDER", currentNumber.isEmpty() ? "---" : currentNumber);. This substitutes the placeholder with the actual, current value of the currentNumber string.

  3. Loop Execution: The loop() function ensures that both the network and the keypad are continuously monitored:

    • server.handleClient(): Processes incoming web requests (like initial page loads or refresh requests).

    • handleKeypad(): Checks for physical keypad presses.

This decoupled and non-blocking architecture allows the ESP32 to efficiently manage both external hardware interaction and network communication simultaneously.

5. Hands On

This is the expected HTML page, showcasing the IP:

Whenever the keypad is pressed, the Serial Monitor will also show which key was pressed:

With the current setup using the HTML meta refresh tag (<meta http-equiv=”refresh” content=”1″>), the system exhibits decoupled asynchronous behavior. When you press a key on the keypad, the handleKeypad() function immediately updates the global variable currentNumber and logs the event to the Serial Monitor, but it does not send any data back to the browser. The web page, which you see in your first image, ignores the keypad presses until the 1-second refresh timer in the browser expires. At that moment, the browser automatically requests the entire page again, the ESP32 rebuilds the HTML with the current state of the currentNumber variable, and only then do you see the accumulated digits appear on the screen, resulting in a slight, but consistent, delay between pressing a key and seeing the update.

6. Lab Recap

This project successfully integrates physical input with a web interface:

  • The Keypad Library is used for non-blocking reading of the 4×3 matrix.
  • Pressed digits (0-9) are accumulated in the currentNumber
  • The special keys (∗ and #) trigger a reset of the string.
  • The ESP32 web server serves an HTML page with clean CSS, where the keypad value is injected into a KEY_VALUE_PLACEHOLDER before being sent to the browser.
  • Calling send() inside handleKeypad() ensures the web page updates immediately upon any key press, providing a real-time user experience.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top