Building an IoT Passenger Counter
ESP32 · MQTT · Express.js · React
A complete step-by-step guide for Windows
Overview
This guide walks you through building a real-time passenger
counter using a single IR sensor connected to an ESP32 microcontroller. Sensor
events are published over WiFi using the MQTT protocol to a Mosquitto broker
running on Windows. An Express.js backend subscribes to the broker and pushes
live updates to a React dashboard via WebSocket.
|
Component |
Role |
|
ESP32 + IR sensor |
Detects beam breaks, publishes count |
|
Mosquitto broker |
MQTT message router (Windows service) |
|
Express.js server |
Subscribes to MQTT, serves REST + WebSocket |
|
React (Vite) |
Live dashboard — updates without page refresh |
Part 1 — USB Driver Installation
The ESP32 communicates with your PC over USB via a
USB-to-UART bridge chip. You must install the correct driver before Windows can
detect the board.
Step 1 — Identify the bridge chip
Look at your ESP32 board near the USB connector. The chip
will be labelled with one of the following:
|
Chip marking |
Driver needed |
|
CP2102 or CP2104 |
Silicon Labs CP210x VCP |
|
CH340 or CH341 |
WCH CH340 driver |
|
FT232 or FT231 |
FTDI VCP driver |
Step 2 — Download and install the Silicon Labs CP210x driver
1. Open
your browser and go to:
www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers
2. Under
Downloads, select CP210x Windows Drivers and download the ZIP file.
3. Extract
the ZIP. Right-click CP210xVCPInstaller_x64.exe and choose Run as
administrator.
4. Follow
the installer prompts and click Finish.
5. Plug
in your ESP32 via USB.
6. Open
Device Manager (Win + X → Device Manager). You should see Silicon Labs CP210x
USB to UART Bridge (COMx) under Ports (COM & LPT).
7. Note
the COM port number — you will need it in Arduino IDE.
⚠ If your board uses a CH340 chip instead,
download the driver from: www.wch-ic.com/downloads/CH341SER_EXE.html — the
installation steps are identical.
Part 2 — Arduino IDE Setup
Step 1 — Download and install Arduino IDE
8. Go
to www.arduino.cc/en/software
9. Download
the Windows installer (.exe).
10. Run the
installer. Accept the license, keep default options, click Install.
11. Launch
Arduino IDE once installation is complete.
Step 2 — Add ESP32 board support
12. In Arduino
IDE go to File → Preferences.
13. Find the
Additional Board Manager URLs field and paste:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
14. Click OK.
15. Go to Tools
→ Board → Boards Manager.
16. Search for
esp32 and install the package by Espressif Systems.
17. Once
installed, go to Tools → Board → ESP32 Arduino → ESP32 Dev Module.
18. Set Tools →
Port to the COM port you noted during driver installation.
19. Set Tools →
Upload Speed to 115200 for reliable uploads.
ℹ If upload fails, hold the BOOT button on the
ESP32 as soon as you see Connecting... in the Arduino IDE console, then release
once uploading begins.
Step 3 — Install required libraries
Go to Sketch → Include Library → Manage Libraries and
install each of the following:
|
Library |
Purpose |
|
PubSubClient by Nick O'Leary |
MQTT publish and subscribe for ESP32 |
|
ArduinoJson by Benoit Blanchon |
JSON serialization for MQTT payloads |
Search the library name, click the result, then click
Install.
Part 3 — IR Sensor Wiring
Components
•
ESP32 Dev Module
•
1× IR obstacle sensor module (with OUT, VCC, GND pins)
•
Breadboard and male-to-male jumper wires
Connections
|
IR sensor pin |
Connect to ESP32 |
|
VCC |
3.3V or 5V (check your module — most accept both) |
|
GND |
GND |
|
OUT |
GPIO 34 |
ℹ GPIO 34 is input-only on the ESP32 — it has
no internal pull-up. If your readings are noisy, wire a 10kΩ resistor between
GPIO 34 and 3.3V.
Part 4 — ESP32 Firmware
Create a new sketch in Arduino IDE (File → New), paste the
code below, update your WiFi credentials and broker IP, then upload.
Find your PC IP address
Open Command Prompt and run:
ipconfig
Look for IPv4 Address under your WiFi adapter. Example:
192.168.1.105. Use this as mqttServer in the sketch.
sketch.ino —
paste into Arduino IDE
#include <WiFi.h>
#include <PubSubClient.h>
#define IR_PIN 34
const char* ssid =
"YOUR_WIFI_SSID";
const char* password =
"YOUR_WIFI_PASSWORD";
const char* mqttServer = "192.168.1.105"; // your PC IPv4
const int mqttPort = 1883;
const char* topic =
"bus/passengers";
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
int obstacleCount = 0;
bool lastState =
false;
void setup() {
Serial.begin(115200);
pinMode(IR_PIN, INPUT);
WiFi.begin(ssid,
password);
while (WiFi.status() !=
WL_CONNECTED) delay(500);
Serial.println("WiFi connected");
mqtt.setServer(mqttServer, mqttPort);
while
(!mqtt.connected()) {
mqtt.connect("esp32-bus");
delay(500);
}
Serial.println("MQTT connected");
}
void loop() {
mqtt.loop();
bool beamBroken =
digitalRead(IR_PIN) == LOW;
// Rising edge detection
— count only on the moment beam first breaks
if (beamBroken
&& !lastState) {
obstacleCount++;
Serial.print("Obstacle count: ");
Serial.println(obstacleCount);
String payload =
"{\"count\":" + String(obstacleCount)
+
",\"busId\":\"BUS_01\"}";
mqtt.publish(topic,
payload.c_str());
}
lastState = beamBroken;
delay(50); // debounce
}
After uploading, open Tools → Serial Monitor at 115200 baud.
You should see WiFi connected followed by MQTT connected. Wave your hand in
front of the IR sensor — the count increments and is published to
bus/passengers.
Part 5 — Mosquitto MQTT Broker on Windows
Step 1 — Install Mosquitto
20. Go to
mosquitto.org/download
21. Download
the Windows installer (.exe).
22. Run the
installer with default options.
Step 2 — Create a config file
Create the following folder and config file:
C:\mosquitto\mosquitto.conf
Paste this content into the file:
# Network
listener 1883
protocol mqtt
# Allow anonymous connections (fine for local dev)
allow_anonymous true
# Logging
log_type all
log_dest file C:\mosquitto\mosquitto.log
log_timestamp true
# Persistence
persistence true
persistence_location C:\mosquitto\data\
Create the data folder:
md C:\mosquitto\data
Step 3 — Open port 1883 in Windows Firewall
netsh advfirewall firewall add rule name="Mosquitto
MQTT" ^
dir=in action=allow
protocol=TCP localport=1883
Step 4 — Stop any existing Mosquitto service
net stop mosquitto
Step 5 — Run Mosquitto with your config
cd "C:\Program Files\mosquitto"
mosquitto.exe -c C:\mosquitto\mosquitto.conf -v
You should see: Opening ipv4 listen socket on port 1883.
Step 6 — Verify the broker
Open a second Command Prompt:
mosquitto_sub.exe -h localhost -t "bus/passengers" -v
Open a third Command Prompt and publish a test message:
mosquitto_pub.exe -h localhost -t "bus/passengers" -m
"{\"count\":1}"
The first terminal should display the message. Broker is
confirmed working.
ℹ MQTT Explorer (free GUI at mqtt-explorer.com)
is highly recommended — connect it to localhost:1883 to visually inspect all
topics and messages without using the terminal.
Part 6 — Express.js Backend
Step 1 — Create the project (nodejs needs to be installed in the system)
md bus-counter && cd bus-counter
md server && cd server
npm init -y
npm install express mqtt socket.io cors
Step 2 — Create server.js
server/server.js
const express =
require('express');
const http =
require('http');
const { Server } = require('socket.io');
const mqtt =
require('mqtt');
const cors =
require('cors');
const app = express();
const server = http.createServer(app);
const io = new
Server(server, { cors: { origin: '*' } });
app.use(cors());
app.use(express.json());
let passengerCount = 0;
let history = [];
const mqttClient = mqtt.connect('mqtt://localhost:1883');
mqttClient.on('connect', () => {
console.log('MQTT
connected');
mqttClient.subscribe('bus/passengers');
});
mqttClient.on('message', (topic, message) => {
const data =
JSON.parse(message.toString());
passengerCount =
data.count;
const entry = { ...data,
timestamp: new Date().toISOString() };
history.push(entry);
if (history.length >
100) history.shift();
console.log('Received:',
entry);
io.emit('passengerUpdate', entry);
});
app.get('/passengers', (req, res) => {
res.json({ count:
passengerCount, busId: 'BUS_01' });
});
app.get('/history', (req, res) => {
res.json(history);
});
server.listen(3001, () =>
console.log('Server
running on http://localhost:3001')
);
Step 3 — Run the server
node server.js
You should see: MQTT connected followed by Server running on
http://localhost:3001.
Test the REST endpoint by opening
http://localhost:3001/passengers in your browser.
Part 7 — React Dashboard
Step 1 — Create the Vite + React project
cd ..
npm create vite@latest client -- --template react
cd client
npm install
npm install socket.io-client
Step 2 — Replace src/App.jsx
client/src/App.jsx
import { useEffect, useState } from "react";
import { io } from "socket.io-client";
const socket = io("http://localhost:3001");
const MAX_CAPACITY = 40;
export default function App() {
const [count,
setCount] = useState(0);
const [history,
setHistory] = useState([]);
const [status,
setStatus] =
useState("connecting");
useEffect(() => {
fetch("http://localhost:3001/passengers")
.then(r =>
r.json())
.then(d =>
setCount(d.count));
fetch("http://localhost:3001/history")
.then(r =>
r.json())
.then(setHistory);
socket.on("connect",
() => setStatus("live"));
socket.on("disconnect", () =>
setStatus("disconnected"));
socket.on("passengerUpdate", (data) => {
setCount(data.count);
setHistory(prev
=> [...prev.slice(-99), data]);
});
return () =>
socket.off("passengerUpdate");
}, []);
const pct = Math.round((count / MAX_CAPACITY) *
100);
const barColor = pct
> 90 ? "#E24B4A" : pct > 70 ? "#EF9F27" :
"#1D9E75";
return (
<div style={{
fontFamily: "sans-serif", maxWidth: 600,
margin:
"2rem auto", padding: "0 1rem" }}>
<div style={{
display: "flex", justifyContent: "space-between",
alignItems: "center", marginBottom: "1.5rem" }}>
<h1 style={{
fontSize: 20, fontWeight: 500, margin: 0 }}>
Bus passenger
counter
</h1>
<span style={{
fontSize: 12,
padding: "4px 10px", borderRadius: 20,
background:
status === "live" ? "#EAF3DE" : "#FCEBEB",
color: status === "live" ?
"#3B6D11" : "#A32D2D"
}}>
{status ===
"live" ? "● Live" : "○ Disconnected"}
</span>
</div>
<div style={{
background: "#f5f5f3", borderRadius: 12,
padding: "1.5rem", marginBottom: "1rem", textAlign:
"center" }}>
<p style={{
fontSize: 13, color: "#888", margin: "0 0 8px" }}>
Passengers on
board
</p>
<p style={{
fontSize: 64, fontWeight: 500, margin: "0 0 16px",
lineHeight: 1 }}>{count}</p>
<p style={{
fontSize: 13, color: "#888", margin: "0 0 10px" }}>
of
{MAX_CAPACITY} capacity ({pct}%)
</p>
<div style={{
background: "#ddd", borderRadius: 6,
height: 10, overflow: "hidden" }}>
<div style={{
width: `${pct}%`, height: "100%",
background: barColor, borderRadius: 6,
transition: "width 0.4s, background 0.4s" }}/>
</div>
</div>
<div style={{
display: "grid", gridTemplateColumns: "1fr 1fr 1fr",
gap:
10, marginBottom: "1.5rem" }}>
{[
{ label:
"Status", value: pct > 90
? "Full" : pct > 70 ? "Busy" : "Normal" },
{ label:
"IR events", value: history.length },
{ label:
"Available", value: Math.max(0, MAX_CAPACITY - count) },
].map(({ label,
value }) => (
<div
key={label} style={{ background: "#f5f5f3",
borderRadius: 8, padding: "0.75rem 1rem" }}>
<p style={{
fontSize: 12, color: "#888", margin: "0 0 4px"
}}>{label}</p>
<p style={{
fontSize: 20, fontWeight: 500, margin: 0 }}>{value}</p>
</div>
))}
</div>
<div style={{
border: "0.5px solid #ddd", borderRadius: 10, overflow:
"hidden" }}>
<p style={{
fontSize: 13, fontWeight: 500, padding: "10px 14px",
margin: 0, borderBottom: "0.5px solid #ddd" }}>
Recent IR events
</p>
{history.length
=== 0 && (
<p style={{
fontSize: 13, color: "#888",
padding: "12px 14px", margin: 0 }}>
Waiting for
sensor data...
</p>
)}
{[...history].reverse().slice(0, 8).map((h, i) => (
<div key={i}
style={{ display: "flex", justifyContent: "space-between",
padding: "8px 14px",
borderBottom: "0.5px solid #eee", fontSize: 13 }}>
<span>Count: <strong>{h.count}</strong></span>
<span
style={{ color: "#888" }}>
{new
Date(h.timestamp).toLocaleTimeString()}
</span>
</div>
))}
</div>
</div>
);
}
Step 3 — Run the React app
npm run dev
Open http://localhost:5173 in your browser. The dashboard
connects automatically and updates live whenever the IR sensor fires.
Part 8 — Running Everything Together
Open three separate Command Prompt windows and run each
command:
|
Terminal |
Command |
|
Terminal 1 — MQTT broker |
cd "C:\Program Files\mosquitto" &&
mosquitto.exe -c C:\mosquitto\mosquitto.conf -v |
|
Terminal 2 — Express server |
cd bus-counter\server && node server.js |
|
Terminal 3 — React app |
cd bus-counter\client && npm run dev |
Then open http://localhost:5173 in your browser. Wave your
hand in front of the IR sensor — the count increments live on the dashboard
with no page refresh.
Troubleshooting
|
Problem |
Fix |
|
ESP32 not detected in Device Manager |
Install the correct USB driver (CP210x or CH340). Try a different
USB cable — many are charge-only with no data lines. |
|
Upload fails with exit status 74 |
Hold the BOOT button on the ESP32 while clicking Upload. Release
once you see progress percentages. |
|
MQTT broker port already in use |
Run: net stop mosquitto — a background service is already running
on 1883. |
|
ESP32 connects to WiFi but not MQTT |
Check Windows Firewall — port 1883 must be open. Verify both
devices are on the same WiFi network. |
|
React dashboard shows Disconnected |
Ensure Express server is running on port 3001. Check browser
console for CORS errors. |
|
Count increments multiple times per pass |
Increase the delay(50) debounce value in the sketch to delay(150)
or higher. |
.jpeg)
