Friday, June 12, 2026

Building an IoT Bus Passenger Counter in Public Transport

 

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.

 Pics:




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.