Objective and use case
What you’ll build: A standalone Field Technician’s Local Network Monitor using an ESP32 configured as a Software Access Point (SoftAP). It broadcasts a secure, localized Wi-Fi network to serve a real-time, air-gapped diagnostic dashboard directly to a smartphone or tablet.
Why it matters / Use cases
- Infrastructure-independent diagnostics: Allows technicians to connect directly to equipment in offline environments like remote agricultural fields, deep industrial basements, or new construction sites.
- Safe machine fault logging: Interfaces with industrial fault relays so personnel can safely monitor machine states via browser, avoiding physical exposure to high-voltage control panels.
- Isolated human-machine interface (HMI): Delivers a highly secure, air-gapped configuration portal that cannot be accessed from the public internet, drastically reducing cybersecurity attack surfaces.
- Access control monitoring: Acts as a localized, temporary monitor for server rack doors or secure gates, logging open/close states instantly.
Expected outcome
- The ESP32 reliably broadcasts a WPA2-secured Wi-Fi network (SSID:
ESP32-FieldMonitor) with a client connection time of <2 seconds. - A fully mobile-responsive web dashboard loads without external internet, rendering diagnostic UI elements.
- Real-time hardware status and fault logs update on the client browser with <50ms network latency.
Audience: Industrial IoT Developers, Field Technicians, Maintenance Engineers; Level: Intermediate
Architecture/flow: ESP32 (SoftAP Mode) → Broadcasts WPA2 SSID → Technician Smartphone Connects → ESP32 Web Server → Serves HTML/JS Dashboard & Streams Real-time GPIO Data.
Educational validation note
Before publication, this case passed the Prometeo automated validation gate with status PASS. For this ESP32 DevKitC profile, the project was checked as a PlatformIO project: the validator extracted platformio.ini and src/main.cpp, created a temporary project and ran pio run against platform = espressif32, board = esp32dev and framework = arduino. It also checked article structure, copy/paste-safe ASCII command options, and unsupported stacks such as direct ESP-IDF or non-scoped ESP32 boards.
Published validation evidence
- Automatic result: PASS.
- Parsed structure: 4 sections, 2 tables and 2 code blocks detected before publication.
- Checked code: 1 PlatformIO config + 1 ESP32 source/pio run.
- Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
- Report findings: no blocking findings.
This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ESP32 DevKitC board, wiring, power supply and local WiFi environment.
Educational safety note
This project is an educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.
Conceptual block diagram
High-level view: what enters the system, what each block processes, and what comes out.
Functional architecture
Conceptual signal and responsibility flow between device blocks.
Validation path
Conceptual summary of the tools used to check the published material.
Prerequisites
To successfully complete this tutorial, you will need:
* Basic understanding of C++ programming and microcontroller GPIO concepts.
* PlatformIO IDE installed (either as a Visual Studio Code extension or via the Command Line Interface).
* Basic knowledge of networking concepts, specifically Wi-Fi Access Points (SSID, WPA2) and IP addressing.
* A web browser on a Wi-Fi enabled device (smartphone, tablet, or laptop) to view the dashboard.
Materials
- Microcontroller: ESP32 DevKitC (Exact model: ESP32 DevKitC V4, typically equipped with the ESP32-WROOM-32 module).
- Input Device: Pushbutton or dry contact switch (this will simulate an external machine fault relay or door contact).
- Visual Indicator: Standard 5mm Status LED (e.g., Blue or Green).
- Passive Components: 1x 330Ω resistor (current limiting for the LED).
- Prototyping: Standard standard breadboard and male-to-male jumper wires.
- Connection: Micro-USB cable (must support both power and data transfer; charging-only cables will fail to flash the device).
(Note: Depending on your specific ESP32 DevKitC manufacturer, you may need to install CP210x or CH34x USB-to-UART drivers on your operating system to allow PlatformIO to recognize the device).
Setup/Connection
The hardware setup utilizes the ESP32’s internal pull-up resistors for the contact input, minimizing the need for external passive components. When the contact switch is open, the internal resistor pulls the pin HIGH. When the switch is closed, it connects the pin to Ground (LOW).
Wiring Table:
| Component | ESP32 DevKitC Pin | Connection / Destination | Description |
|---|---|---|---|
| Contact Switch | GPIO 18 | Terminal 1 of Switch | Configured as INPUT_PULLUP. |
| Contact Switch | GND | Terminal 2 of Switch | Pulls GPIO 18 LOW when closed. |
| Status LED | GPIO 19 | LED Anode (Long leg) | Configured as OUTPUT. |
| Status LED | GND | LED Cathode (Short leg) via 330Ω Resistor | Completes the LED circuit safely. |
Important Hardware Notes:
1. Ensure you are using a 330Ω resistor in series with the Status LED to prevent drawing excessive current from the ESP32’s GPIO pin, which could damage the microcontroller.
2. Do not connect the contact switch to any external voltage source. It must act as a “dry contact” (a simple mechanical closure to Ground).
Validated Code
The project requires two files within your PlatformIO workspace. The platformio.ini file configures the build environment, while src/main.cpp contains the application logic.
PlatformIO Configuration
Create or overwrite the platformio.ini file in the root of your project directory with the following configuration. This ensures the correct board definition and serial monitor baud rate are used.
; platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
Application Source Code
Create or overwrite the src/main.cpp file with the following complete, compilable code.
Public preview of the validated file. The complete source is shown to members and in PDF/Print.
// src/main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
// Hardware Pin Definitions
#define CONTACT_PIN 18
#define STATUS_LED_PIN 19
// SoftAP Network Credentials
const char* ssid = "ESP32-FieldMonitor";
const char* password = "admin1234"; // WPA2 requires a minimum of 8 characters
// Initialize the WebServer on port 80
WebServer server(80);
// Global state variables
int currentContactState = HIGH;
int lastContactState = HIGH;
// HTML Dashboard stored in Program Memory (PROGMEM) to save RAM
const char dashboard_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Field Monitor Dashboard</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
background-color: #e9ecef;
margin: 0;
padding: 20px;
}
.card {
background: white;
max-width: 400px;
margin: 40px auto;
padding: 30px;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}
h2 {
color: #343a40;
margin-top: 0;
}
.status-box {
font-size: 1.8em;
font-weight: bold;
margin: 20px 0;
padding: 20px;
border-radius: 8px;
transition: background-color 0.3s, color 0.3s;
}
.loading { background-color: #f8f9fa; color: #6c757d; border: 2px dashed #6c757d; }
.closed { background-color: #d4edda; color: #155724; border: 2px solid #28a745; }
.open { background-color: #f8d7da; color: #721c24; border: 2px solid #dc3545; }
.footer {
margin-top: 20px;
font-size: 0.85em;
color: #6c757d;
}
</style>
</head>
<body>
<div class="card">
<h2>Machine Contact Status</h2>
<div id="contact-state" class="status-box loading">Awaiting Data...</div>
<div class="footer">Auto-refreshing every 500ms via JSON API</div>
</div>
<script>
// Asynchronous function to fetch status from the ESP32
function fetchStatus() {
fetch('/api/status')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
const statusDiv = document.getElementById('contact-state');
// data.contact is 0 when closed (LOW) due to INPUT_PULLUP
if(data.contact === 0) {
statusDiv.innerHTML = "CONTACT CLOSED";
statusDiv.className = "status-box closed";
} else {
statusDiv.innerHTML = "CONTACT OPEN";
statusDiv.className = "status-box open";
}
// ... continues for members in the complete validated source ...// src/main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
// Hardware Pin Definitions
#define CONTACT_PIN 18
#define STATUS_LED_PIN 19
// SoftAP Network Credentials
const char* ssid = "ESP32-FieldMonitor";
const char* password = "admin1234"; // WPA2 requires a minimum of 8 characters
// Initialize the WebServer on port 80
WebServer server(80);
// Global state variables
int currentContactState = HIGH;
int lastContactState = HIGH;
// HTML Dashboard stored in Program Memory (PROGMEM) to save RAM
const char dashboard_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Field Monitor Dashboard</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
background-color: #e9ecef;
margin: 0;
padding: 20px;
}
.card {
background: white;
max-width: 400px;
margin: 40px auto;
padding: 30px;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}
h2 {
color: #343a40;
margin-top: 0;
}
.status-box {
font-size: 1.8em;
font-weight: bold;
margin: 20px 0;
padding: 20px;
border-radius: 8px;
transition: background-color 0.3s, color 0.3s;
}
.loading { background-color: #f8f9fa; color: #6c757d; border: 2px dashed #6c757d; }
.closed { background-color: #d4edda; color: #155724; border: 2px solid #28a745; }
.open { background-color: #f8d7da; color: #721c24; border: 2px solid #dc3545; }
.footer {
margin-top: 20px;
font-size: 0.85em;
color: #6c757d;
}
</style>
</head>
<body>
<div class="card">
<h2>Machine Contact Status</h2>
<div id="contact-state" class="status-box loading">Awaiting Data...</div>
<div class="footer">Auto-refreshing every 500ms via JSON API</div>
</div>
<script>
// Asynchronous function to fetch status from the ESP32
function fetchStatus() {
fetch('/api/status')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
const statusDiv = document.getElementById('contact-state');
// data.contact is 0 when closed (LOW) due to INPUT_PULLUP
if(data.contact === 0) {
statusDiv.innerHTML = "CONTACT CLOSED";
statusDiv.className = "status-box closed";
} else {
statusDiv.innerHTML = "CONTACT OPEN";
statusDiv.className = "status-box open";
}
})
.catch(error => {
console.error('Error fetching status:', error);
const statusDiv = document.getElementById('contact-state');
statusDiv.innerHTML = "CONNECTION LOST";
statusDiv.className = "status-box loading";
});
}
// Poll the API every 500 milliseconds
setInterval(fetchStatus, 500);
// Initial fetch immediately on load
window.onload = fetchStatus;
</script>
</body>
</html>
)rawliteral";
// Route Handler: Serve the main HTML dashboard
void handleRoot() {
server.send(200, "text/html", dashboard_html);
Serial.println("Dashboard accessed by a client.");
}
// Route Handler: Serve the JSON API for the dashboard to consume
void handleApiStatus() {
// Construct a simple JSON string manually
String jsonPayload = "{\"contact\": " + String(currentContactState) + "}";
server.send(200, "application/json", jsonPayload);
}
// Route Handler: Handle 404 Not Found
void handleNotFound() {
server.send(404, "text/plain", "404: Not Found");
}
void setup() {
// Initialize Serial Monitor
Serial.begin(115200);
delay(1000); // Allow serial to stabilize
Serial.println("\n--- ESP32 Field Monitor Initialization ---");
// Configure Hardware Pins
pinMode(CONTACT_PIN, INPUT_PULLUP);
pinMode(STATUS_LED_PIN, OUTPUT);
// Read initial state
currentContactState = digitalRead(CONTACT_PIN);
lastContactState = currentContactState;
// Set initial LED state (ON when contact is closed/LOW)
digitalWrite(STATUS_LED_PIN, (currentContactState == LOW) ? HIGH : LOW);
// Configure Wi-Fi in Access Point (SoftAP) mode
Serial.print("Configuring Access Point...");
WiFi.softAP(ssid, password);
IPAddress IP = WiFi.softAPIP();
Serial.println(" Ready!");
Serial.print("SoftAP SSID: ");
Serial.println(ssid);
Serial.print("SoftAP IP Address: ");
Serial.println(IP);
// Define Web Server Routing
server.on("/", HTTP_GET, handleRoot);
server.on("/api/status", HTTP_GET, handleApiStatus);
server.onNotFound(handleNotFound);
// Start the Web Server
server.begin();
Serial.println("HTTP Web Server started.");
}
void loop() {
// Handle incoming HTTP client requests
server.handleClient();
// Read the physical contact state
currentContactState = digitalRead(CONTACT_PIN);
// Detect state changes to update the LED and log to Serial
if (currentContactState != lastContactState) {
// Debounce delay (basic implementation)
delay(50);
currentContactState = digitalRead(CONTACT_PIN);
if (currentContactState != lastContactState) {
if (currentContactState == LOW) {
Serial.println("EVENT: Contact CLOSED (Active).");
digitalWrite(STATUS_LED_PIN, HIGH); // Turn LED ON
} else {
Serial.println("EVENT: Contact OPEN (Inactive).");
digitalWrite(STATUS_LED_PIN, LOW); // Turn LED OFF
}
lastContactState = currentContactState;
}
}
}
Build/Flash/Run commands
Use the PlatformIO Core CLI to compile, upload, and monitor your project. Open your terminal in the project’s root directory (where platformio.ini is located) and execute the following commands.
| Command | Purpose |
|---|---|
pio run | Compiles the project and verifies all dependencies and syntax. |
pio run --target upload | Compiles and flashes the compiled firmware to the ESP32 DevKitC. |
pio device monitor | Opens the serial monitor to view runtime logs at 115200 baud. |
Execution Workflow:
1. Connect the ESP32 DevKitC to your computer via the micro-USB cable.
2. Run pio run to ensure the code compiles without syntax errors.
3. Run pio run --target upload. (If the upload fails to connect, press and hold the BOOT button on the ESP32 DevKitC when you see “Connecting…” in the terminal).
4. Run pio device monitor to observe the initialization sequence and verify the SoftAP IP address.
Step-by-step Validation
Follow these checkpoints to ensure the prototype operates exactly as intended.
- Verify Serial Initialization
- Action: Observe the terminal output immediately after running
pio device monitoror pressing theEN(Reset) button on the ESP32. - Expected observation: The terminal prints “— ESP32 Field Monitor Initialization —“, followed by the SSID
ESP32-FieldMonitorand the IP192.168.4.1. - Pass condition: The ESP32 does not crash or enter a reboot loop.
- Action: Observe the terminal output immediately after running
- Verify SoftAP Broadcast
- Action: Open the Wi-Fi settings on a smartphone or laptop.
- Expected observation: A network named
ESP32-FieldMonitorappears in the list of available networks. - Pass condition: You can successfully connect to the network using the password
admin1234.
- Verify Web Dashboard Loading
- Action: Open a web browser on the connected device and navigate to
http://192.168.4.1. - Expected observation: The “Field Monitor Dashboard” loads, showing a styled card. The serial monitor logs “Dashboard accessed by a client.”
- Pass condition: The UI renders correctly without broken CSS.
- Action: Open a web browser on the connected device and navigate to
- Verify Physical Input and LED Output
- *Action
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.




