CNG Multidevice Sale Tracker

by Crazy_ Whale in Circuits > Microcontrollers

32 Views, 1 Favorites, 0 Comments

CNG Multidevice Sale Tracker

Screenshot 2026-07-04 114033.png
photo_2026-07-04_10-55-01.jpg

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.

Supplies

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.

Components Used

  1. ESP32 Dev Board
  2. RS-485 Module
  3. 4×4 Matrix Keypad
  4. 16×2 LCD Display
  5. RS-485 cable
  6. USB-A to Micro-USB
  7. Dupont jumper wires
  8. Breadboard or PCB
  9. 5 V power supply
  10. Project enclosure box


System Architecture

ChatGPT Image Apr 4, 2026, 09_11_02 PM.png
Copilot_20260401_222037.png
ChatGPT Image Apr 4, 2026, 08_56_10 PM.png

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
VCC 3.3 V
GND GND
RO (Receiver Out) GPIO 16 (Serial2 RX)
DI (Driver In) GPIO 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)

  1. | A | Dispenser RS-485 terminal A
  2. | 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 Pin ESP32
VCC VIN pin
GND GND
SDA GPIO 21
SCL GPIO 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 0 GPIO 13
Row 1 GPIO 12
Row 2 GPIO 14
Row 3 GPIO 27
Col 0 GPIO 26
Col 1 GPIO 25
Col 2 GPIO 33
Col 3 GPIO 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

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

Wiring Diagram & Pin Reference

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" (version 2.0.x or later).

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.

6. Note down:

  1. `REG_TX_STATUS` — the register that becomes `1` after a completed fill
  2. `REG_QTY_HI / REG_QTY_LO` — quantity in kg × 100
  3. `REG_AMT_HI / REG_AMT_LO` — amount in ₹ × 100
  4. `REG_FLOW_HI / REG_FLOW_LO` — flow rate (may be 0 on older firmware)

Known presets already included in config.h:

  1. Kranti (baud 9600), Adani (baud 19200), NeoGas/Elgi (baud 9600),
  2. Bennett/Wayne (baud 38400). If you have one of these, just uncomment the
  3. 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:

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

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

Dashboard Pages Overview

Screenshot 2026-06-29 222235.png
Screenshot 2026-07-01 110425.png

Dashboard: Live sale count, revenue today, active devices

Devices: All registered dispensers with online/offline status

Sale History: Every sale record with filters

Vehicle Tracking: Per-vehicle fill history and total spend

Daily Report: Day-by-day totals with CSV export

Weekly Report: Week-over-week revenue chart

Monthly Report: Monthly totals and trends

Settings: Account management

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/`:


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
// #define DISPENSER_KRANTI
// #define DISPENSER_ADANI
// #define DISPENSER_NEOGAS
// #define DISPENSER_BENNETT


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:

  1. Tools → Board → select "ESP32 Dev Module"
  2. Tools → Port → select the COM port for your ESP32(Windows: `COM3`, `COM4`, etc. — check Device Manager if unsure.
  3. Tools → Upload Speed → `921600` (or lower if uploads fail)
  4. 3. Click Upload (→ arrow).
  5. 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`

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.

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.


9.3 — Test WiFi Reconnect (Optional)

1. Disconnect the WiFi router briefly (5 seconds).

2. LCD briefly shows `CNG Ready[NoWiFi]`.

3. Reconnect the router — within 30 seconds the ESP32 reconnects automatically and

shows `CNG Ready [WiFi]` again.

4. Any sales made while offline are buffered (up to 10 records) and flushed

automatically once WiFi is restored.

Scaling to Multiple Dispensers

Each additional dispenser arm gets its own ESP32 unit. The process is identical to with two differences per unit:

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:

| Folder | Device ID | Station | Arm |

| `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 |

Copy the contents of the right folder's `config.h` over the template in `esp32_dispenser_client/config.h` before flashing each unit.


10.2 — Each Unit is Independent

  1. Each ESP32 has its own RS-485 connection to its own dispenser.
  2. All units share the same WiFi and backend URL.
  3. The backend uses `deviceId` to distinguish sales from different arms.
  4. 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.

Troubleshooting

LCD shows nothing / all black squares

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

Keypad keys not registering or wrong characters

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

Modbus: "No valid response" in modbus_test.ino

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

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

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

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

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

Sales not appearing in the dashboard

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

Sale shows wrong quantity or amount (e.g. 100× too large)

  1. The firmware reads values as integers × 100 (e.g., 5.20 kg is stored as 520).
  2. If your dispenser stores raw kg without the ×100 scale factor, divide the rawvalue by 1 instead of 100. Edit the `modbusReadTransaction()` function:
out->quantityKg = rawQty / 1.0f; // if dispenser stores actual kg
out->quantityKg = rawQty / 100.0f; // if dispenser stores kg × 100 (default)

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

  1. Make sure MongoDB is running: `mongod --dbpath /data/db` (Linux/Mac)or start the MongoDB service in Windows Services.
  2. If using Atlas, confirm the `MONGO_URI` in `.env` is the full Atlas connectionstring 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.

Operator Quick-Reference Card

Cut this out and post it at each dispenser station.

┌──────────────────────────────────────────────┐
│ CNG Dispenser — Operator Guide │
├──────────────────────────────────────────────┤
│ STANDBY "CNG Ready [WiFi]" │
│ Press * to start new vehicle │
├──────────────────────────────────────────────┤
│ ENTER VEH Type vehicle number: │
│ (multi-tap) 1=1EFG 2=2HIJ 3=3KLM │
│ 4=4NOP 5=5QRS 6=6TUV │
│ 7=7WX 8=8YZ 9=9 0=0 │
│ │
│ * = backspace (remove last) │
│ ** = cancel → back to standby │
│ # = CONFIRM vehicle number │
├──────────────────────────────────────────────┤
│ DISPENSING "Dispensing… Veh:GJ01AB1234" │
│ Fill the vehicle normally. │
│ System auto-detects when done. │
│ ** = cancel if needed │
├──────────────────────────────────────────────┤
│ DONE "5.20kg Rs442 / Sent! …" │
│ Sale sent automatically. │
│ 2 s → ready for next vehicle │
├──────────────────────────────────────────────┤
│ LCD shows [NoWiFi] → sales are buffered │
│ and will be sent when WiFi returns. │
└──────────────────────────────────────────────┘


#Appendix: Generating Demo Data

To populate the dashboard with 30+ days of synthetic sales for a demo or presentation:

node scripts/generate_demo_sales.js

To export analytics reports to CSV files in the `dataset/` directory:

node scripts/export_reports.js

Connect Me

Support Me

THANK YOU