CNG Multidevice Sale Tracker

Published Jul 01, 2026
 38 hours to build
 Expert

An ESP32-powered IoT system that plugs into any CNG fuel dispenser's RS-485 Modbus port, automatically captures every sale vehicle number, quantity, and amount and beams it over WiFi to a live React dashboard, giving gas station operators real-time visibility, vehicle tracking, and daily/weekly/monthly revenue reports across all dispenser arms from a single screen.

display image

Components Used

4x4 Matrix Keypad
4x4 Matrix Keypad
1
ESP32 DevKit Board
1
16x2 LCD Display Module
1
Mini Breadboard
1
DB9 Female Serial Connector
1
Plastic Enclosure Box
1
MAX3232 RS232-TTL Converter Module
1
40-Pin Male Header
1
40-Pin Female Header
1
Jumper Wire
1
18650 Battery
2
18650 Battery Holder
1
Zero PCB
1
Description

CNG Multi-Device Sale Tracker: Complete Build Guide

What you'll build: A real-time IoT system that connects CNG (Compressed Natural Gas) fuel dispensers to a cloud dashboard. Each dispenser gets an ESP32 that reads sale data over RS-485 Modbus, accepts vehicle number input via a 4×4 keypad, shows status on a 16×2 LCD, and POSTs every completed sale to a Node.js backend. A React dashboard gives operators a live view of all dispensers, vehicle tracking, and daily/weekly/monthly reports.

Step 1 — Parts List

ComponentQty
ESP32 Dev Board1
RS-485 Module1
4×4 Matrix Keypad1
16×2 LCD Display1
RS-485 cable1
USB-A to Micro-USB1
Dupont jumper wires 
Breadboard or PCB1
5 V power supply1
Project enclosure box1

For the Server (one-time)

ComponentNotes
WiFi routerAll ESP32 units must reach the same network as the server
PC / LaptopRunning Node.js backend (or a Raspberry Pi / cloud VM)
MongoDBLocal install or MongoDB Atlas free tier

 Step 2 — System Architecture

Understanding the data flow will save you debugging time later.

 

How a sale is recorded (step-by-step data flow) 

1. Operator presses `*` on the keypad → ESP32 enters vehicle number mode.

2. Operator types the vehicle plate using multi-tap (Nokia phone style) and presses `#`.

3. ESP32 shows "Dispensing…" and starts polling the dispenser via Modbus RS-485 every 50 ms.

4. When the dispenser finishes filling, it sets Modbus register `REG_TX_STATUS = 1`.

5. ESP32 reads quantity (kg), amount (₹), and flow rate from Modbus registers.

6. ESP32 POSTs a JSON record to `POST /api/sales` over WiFi.

7. Backend saves to MongoDB and pushes a real-time update to all open dashboards via SSE.

8. LCD shows "Sent! GJ01AB1234 / Next vehicle…" for 2 seconds, then resets.

Step 3 — Hardware Assembly

3.1 — Prepare the ESP32

1. Plug the ESP32 into your breadboard so the two rows of pins straddle the center gap.

2. Leave at least 3 rows of free breadboard above and below the ESP32 for jumper wires.

3. If your ESP32 doesn't come with headers soldered, solder 2.54 mm male headers now.

 

3.2 — Wire the RS-485 Module

The RS-485 module (MAX485 / SP3485) translates the ESP32's TTL UART signals to the differential A/B signals used by Modbus.

RS-485 Module Pin ESP32
VCC3.3 V
GNDGND
RO (Receiver Out)GPIO 16 (Serial2 RX)
DI (Driver In)PIO 17 (Serial2 TX)
DE (Driver Enable)If auto-direction: bridge to RE. If manual: GPIO 4
RE (Receiver Enable) Bridge to DE (tie them together)
  • | A | Dispenser RS-485 terminal A
  • | B | Dispenser RS-485 terminal B 

Auto-direction modules: (like the widely available blue MAX485 breakout) have DE and RE

already bridged internally. Just connect RO→GPIO16, DI→GPIO17, and the A/B pair to the

dispenser. Set `RS485_DE_PIN -1` in `modbus_test.ino` if testing.

Wiring the A/B cable to the dispenser: Locate the Modbus RS-485 port on your

dispenser (usually labeled "RS-485", "COM", or "Modbus" on the terminal strip).

Connect A→A, B→B, and share a GND wire between the ESP32 ground and the dispenser

chassis ground to prevent noise.

 

3.3 — Wire the 16×2 I2C LCD

The LCD uses only 4 wires thanks to the I2C backpack.

CD I2C Backpack PinESP32
VCCVIN pin
GNDGND
SDA GPIO 21
SCLGPIO 22
  •  If the display shows all black squares after power-on, turn the contrast pot on the
  • back of the I2C backpack. If `0x27` doesn't work, try `0x3F` in `config.h`.

 

3.4 — Wire the 4×4 Matrix Keypad

The keypad has 8 pins — 4 rows and 4 columns. Pin 1 is usually marked with a triangle.

Keypad Pin  ESP32
Row 0GPIO 13
Row 1GPIO 12 
Row 2GPIO 14
Row 3GPIO 27
Col 0GPIO 26
Col 1GPIO 25 
Col 2GPIO 33
Col 3GPIO 32 
  • Pin 1 is the leftmost pin when the keypad faces you with keys visible.
  • The row pins are typically the first 4, column pins are the last 4.
  • Check your keypad's datasheet if unsure — a multimeter in continuity mode will confirm which pins short when you press a key.


 3.5:  Power

  • Power the ESP32 via its USB port during development and testing.
  • In the field, use a 5 V / 2 A DC supply connected to the VIN and GND pins,
  • or use a USB wall adapter with a permanently attached cable.
  • The LCD and keypad draw minimal current the ESP32's own regulators can supply them.

Step 4 — Wiring Diagram & Pin Reference


Step 5: Test the Hardware Before Soldering

Always verify each component with the dedicated test sketches before you solder everything into a permanent enclosure. This saves hours of debugging.


 5.1: Install the Arduino IDE and ESP32 Board Support

1. Download Arduino IDE 2.x.

2. Open File → Preferences and add this URL to "Additional Boards Manager URLs":

   https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
 

3. Go to Tools → Board → Boards Manager, search for `esp32`, and install

   "esp32 by Espressif Systems".
 

4. Install required libraries via Sketch → Include Library → Manage Libraries:

  `Keypad` by Mark Stanley & Alexander Brevig

  `LiquidCrystal I2C` by Frank de Brabander

 5.2 — Test the LCD

1. Open `lcd_test.ino` in Arduino IDE.

2. Select your board: Tools → Board → ESP32 Dev Module.

3. Select the correct COM port under Tools → Port.

4. Click Upload (→ arrow icon).

5. Expected result: LCD backlight turns on, Row 0 shows "LCD Test OK",

   Row 1 shows "Hello CNG!".

6. If nothing displays: check SDA/SCL wiring, adjust contrast pot, try I2C address `0x3F`.


 5.3 — Test the Keypad

1. Open `firmware/test_scripts/keypad_test.ino`.

2. Upload to the ESP32.

3. Open Serial Monitor at 115200 baud.

4. Press each of the 16 keys one at a time.

5. Expected result: Each key press prints the correct character

   (`1 2 3 A / 4 5 6 B / 7 8 9 C / * 0 # D`).

 

5.4 — Test the Modbus RS-485 Connection to the Dispenser

1. Open `modbus_test.ino`.

2. Upload and open Serial Monitor at 115200 baud.

3. Find the baud rate: Press `b` → the tool tries 1200/2400/4800/9600/19200/38400/57600.

   Note which baud gets a "RESPONSE RECEIVED ✓".

4. Find the slave address: Update `MODBUS_BAUD` at the top of the sketch with the

   found value, re-upload, then press `a` → scan addresses 1–32.

5. Scan registers: Press `s` → all non-zero registers print.

   Then do a test dispense (fill a few kg), press `s` again.

   The registers that changed are your transaction registers.

Known presets already included in config.h:

  • Kranti (baud 9600), Adani (baud 19200), NeoGas/Elgi (baud 9600),
  • Bennett/Wayne (baud 38400). If you have one of these, just uncomment the
  • right `#define DISPENSER_XXX` line — no register scanning needed.

 

5.5 — Test WiFi + Backend Connectivity

1. Open `wifi_api_test.ino`.

2. Fill in `WIFI_SSID`, `WIFI_PASSWORD`, and `API_BASE_URL` at the top.

3. Upload and open Serial Monitor at 115200 baud.

4. On boot it runs 5 tests automatically:

  •    Test 1: Backend reachable (expects HTTP 200)
  • Test 2: Device found in registry
  • Test 3: POST a synthetic sale (expects HTTP 201)
  • Test 4: Heartbeat PATCH (expects HTTP 200)
  • Test 5: Retry logic with a bad URL

Run this test after Step 6 (backend setup) so the API is running before you test.

Step 6: Dashboard Pages Overview

Step 7 — Firmware Configuration & Flash

7.1 — Understand the config.h File

Every ESP32 unit is flashed with the **same sketch but a different `config.h`. The pre-built per-device configs are in `firmware/devices/`:

firmware/

  devices/

    ESP32_A1/config.h   ← Station Alpha, Arm 1

    ESP32_A2/config.h   ← Station Alpha, Arm 2

    ESP32_B1/config.h   ← Station Beta, Arm 1

    ESP32_B2/config.h   ← Station Beta, Arm 2

  esp32_dispenser_client/

    config.h            ← master template (edit this for flashing)

    esp32_dispenser_client.ino

 

7.2: Configure for Your First Dispenser

Open `firmware/esp32_dispenser_client/config.h` and work through the four steps marked inside the file:

Step 1:  Select your dispenser brand (uncomment exactly one):

#define DISPENSER_GENERIC    // default — fill in custom map

 

Step 2: Set the unique device ID (change for every unit):

#define DEVICE_ID   "ESP32_A1"   // ← MUST match what you seeded in MongoDB

 

Step 3: Set the Modbus slave address:

#define MODBUS_ADDR   1   // ← use 'a' scan from modbus_test.ino to find this

 

Step 4: Set WiFi and API (same for all units at one station):**

 

#define WIFI_SSID       "YourStationWiFi"

#define WIFI_PASSWORD   "YourPassword"

#define API_BASE_URL    "http://192.168.1.100:5000"   // ← your server's LAN IP

 

If you chose `DISPENSER_GENERIC`, also fill in the register addresses you found in Step 5.4:

 

#define MODBUS_BAUD     9600     // ← from baud scan

#define REG_TX_STATUS   0x0000   // ← register that = 1 after a fill

#define REG_QTY_HI      0x0001   // ← quantity high word

#define REG_QTY_LO      0x0002   // ← quantity low word

#define REG_AMT_HI      0x0003   // ← amount high word

#define REG_AMT_LO      0x0004   // ← amount low word

 

#define MODBUS_REG_COUNT 15      // ← total regs to read in one FC03 call

 

7.3: Flash the Firmware

1. Connect the ESP32 to your PC via USB.

2. In Arduino IDE:

  •    Tools → Board → select "ESP32 Dev Module"
  • Tools → Port → select the COM port for your ESP32 (Windows: `COM3`, `COM4`, etc. — check Device Manager if unsure. macOS/Linux: `/dev/tty.SLAB_USBtoUART` or `/dev/ttyUSB0`)
  • Tools → Upload Speed → `921600` (or lower if uploads fail)

3. Click Upload (→ arrow).

4. You may need to hold the **BOOT** button on the ESP32 while upload starts

   (only required on some boards without auto-reset circuitry).
 

7.4 — Verify on Boot

Open Serial Monitor at 115200 baud immediately after flashing.

You should see: CNG Dispenser Client  v4.0             

  Device  : ESP32_A1                      

 Modbus  : addr=1                        

 WiFi    : YourStationWiFi               

 API     : http://192.168.1.100:5000     

 

[WiFi] Connecting to "YourStationWiFi"...

[WiFi] Connected  IP=192.168.1.105

[Device] Fetching _id from http://192.168.1.100:5000/api/devices/lookup/ESP32_A1

[Device] Resolved _id=64f3a8c9b2e1234567890abc

[Heartbeat] online  code=200

 

  • And the LCD shows: `CNG Ready [WiFi]` / `Press * to start`

 

Step 8 — Register the Devices in the Dashboard

The ESP32 firmware looks up its own MongoDB `_id` by calling`GET /api/devices/lookup/{DEVICE_ID}`. This only works if the device document already exists in the database.

 

8.1 — Check Seeded Devices

The `seed_devices.js` script (Step 6.5) already created 4 devices:

`ESP32_A1`, `ESP32_A2`, `ESP32_B1`, `ESP32_B2`.

Verify them in the Devices page of the dashboard, or via the API: curl http://localhost:5000/api/devices

8.2 — Add a Custom Device

If your dispenser has a different ID than the seeded defaults, add it via the dashboard

Devices page (click "+ Add Device") or with the API:

curl -X POST http://localhost:5000/api/devices \

  -H "Content-Type: application/json" \

  -d '{

    "deviceId": "ESP32_C1",

    "station": "Station Charlie",

    "arm": "Arm 1",

    "description": "North pump island"

  }'

The `deviceId` here must exactly match `DEVICE_ID` in `config.h`.


 8.3 — Confirm Device Goes Online

After flashing and connecting to WiFi, the ESP32 sends a heartbeat PATCH every 20 seconds.

Within 20 seconds of boot, the device card in the dashboard should show a green "online" badge.

Step 9:  Connect Everything: Full System Test

With hardware wired, backend running, frontend open, and firmware flashed, run a complete end-to-end test to confirm the entire pipeline works.
 

9.1 — Simulate a Sale from the Keypad

1. On the ESP32 keypad, press `*` → LCD shows `Enter Veh No: / >_`

2. Type a vehicle number using multi-tap:

   - Press `4` once → `4`

   - Press `5` twice → `5`, `5J` → wait 1.5 s to commit `J`

   - Press `0` once → `0`

   - Press `1` once → `1`

   - Continue until you have e.g., `GJ01AB1234`

   - Press `#` to confirm

3. LCD shows `Dispensing… / Veh:GJ01AB1234`

4. Trigger a Modbus transaction on your dispenser (do a short fill, or use

   `modbus_test.ino` → press `w` to write `REG_TX_STATUS = 0` after a real fill to

   simulate acknowledgment)

5. LCD shows `Sent! 5.20kg / Rs442 GJ01AB1234`


 

9.2 — Verify in the Dashboard 

1. Open the Sale History page — the new sale should appear at the top.

2. Open the Dashboard: today's sale count and revenue should be updated.

3. Open the Vehicle Tracking page and search for `GJ01AB1234` — the fill history

   should show the record.

Step 10 — Scaling to Multiple Dispensers

Each additional dispenser arm gets its own ESP32 unit. The process is identical to


 10.1 — Create a Per-Device config.h

Copy `firmware/esp32_dispenser_client/config.h` and change only:

#define DEVICE_ID   "ESP32_A2"   // ← unique ID for this unit

#define MODBUS_ADDR  1           // ← this dispenser's Modbus address

                                 //   (usually 1 per dispenser, since each

                                 //    ESP32 has its own RS-485 bus)

 

WiFi credentials and `API_BASE_URL` stay the same across all units.

  • The pre-made configs in `firmware/devices/` are ready to use:
  • `firmware/devices/ESP32_A1/config.h` | ESP32_A1 | Alpha | 1 |
  • `firmware/devices/ESP32_A2/config.h` | ESP32_A2 | Alpha | 2 |
  • `firmware/devices/ESP32_B1/config.h` | ESP32_B1 | Beta | 1 |
  • `firmware/devices/ESP32_B2/config.h` | ESP32_B2 | Beta | 2 |


 10.2 — Each Unit is Independent

  • Each ESP32 has its own RS-485 connection to its own dispenser.
  • All units share the same WiFi and backend URL.
  • The backend uses `deviceId` to distinguish sales from different arms.
  • The dashboard shows all devices in a single unified view.


 10.3 — RS-485 Bus Topology

Each ESP32 connects to **one dispenser only** (point-to-point). This is the simplest and most reliable topology. If your dispenser manufacturer already daisy-chains multiple dispenser heads on one RS-485 bus (common in 2-arm dispensers), you can use Modbus addresses 1 and 2 on the same RS-485 pair, but you'll need only one ESP32 with a modified sketch — contact the dispenser manufacturer for the register map in that case.

Step 11 — Troubleshooting


 LCD shows nothing / all black squares

  • Turn the small blue contrast potentiometer on the back of the I2C backpack.
  • Verify SDA→GPIO21, SCL→GPIO22.
  • Try changing `LCD_I2C_ADDR` from `0x27` to `0x3F` in `config.h`.
  • Run an I2C scanner sketch to find the actual address.


 Keypad keys not registering or wrong characters

  • Re-check the row/column GPIO assignments against the physical keypad ribbon.
  • Use a multimeter in continuity mode: pressing a key should short one row pin to one column pin.
  • Pin 1 of the keypad (marked with a dot or arrow) is row 0.


 Modbus: "No valid response" in modbus_test.ino

  • Check A/B polarity — swap them if needed.
  • Verify the RS-485 module DE/RE pins are bridged.
  • Confirm the dispenser is powered and its Modbus port is active.
  • Run baud scan (`b`) — your dispenser may use 19200 or 38400, not 9600.
  • Check the dispenser manual for the RS-485 terminal location — some models label it "COM" or have it inside a locked service panel.


ESP32 shows "CNG Ready[NoWiFi]" and never connects

  • Confirm SSID and password in `config.h` are correct (case-sensitive).
  • Check the ESP32 is within range of the access point.
  • ESP32 only supports 2.4 GHz WiFi — if your router is 5 GHz only, it won't connect.
  • If you changed `WIFI_RECONNECT_INTERVAL_MS`, ensure it's not set too short.


 Serial Monitor shows "Device _id not found" or fetch fails

  • Confirm the backend is running and reachable at `API_BASE_URL`.
  • Verify the device was seeded: `node scripts/seed_devices.js`.
  • Check the `DEVICE_ID` in `config.h` exactly matches the `deviceId` in MongoDB (case-sensitive: `ESP32_A1` ≠ `esp32_a1`).
  • Test with `wifi_api_test.ino` → Test 2 will report if the device is missing.


 Sales not appearing in the dashboard

  • Check the backend terminal for incoming POST requests — each sale should print: `[hh:mm:ss] POST /api/sales`
  • Confirm the POST returns HTTP 201 (check Serial Monitor: `[Flush] OK veh=...`).
  • If HTTP 4xx, the `deviceId` in the POST body may not match any registered device.
  • Refresh the dashboard — the SSE live update may not have connected; a manual refresh will always show the latest data from MongoDB.

Backend won't start — "MongoServerError: connect ECONNREFUSED"

  • Make sure MongoDB is running: `mongod --dbpath /data/db` (Linux/Mac) or start the MongoDB service in Windows Services.
  • If using Atlas, confirm the `MONGO_URI` in `.env` is the full Atlas connection string including username, password, and cluster hostname.


 DEVICE_ID is not defined compile error

  • This means you are compiling the main sketch without `config.h` present in the same folder. Make sure both `esp32_dispenser_client.ino` and `config.h` are in the same directory, and that directory is what you opened in Arduino IDE.

 

 

Codes

Downloads

firmware Download
Comments
Ad