Automated perfume dispenser with touchscreen UI, QR payment, and remote management
A production vending machine that sells perfume doses on the street. The customer presses a physical button to select a fragrance, a Payme QR code appears on the TFT display, they scan and pay, and the machine dispenses 2 spray doses via servo-controlled nozzles.
This is a complete commercial product — firmware, payment backend, remote price management, and hardware control.
What it does:
- 4 perfume slots with physical button selection and LVGL touchscreen UI
- Generates Payme QR codes directly on a 320×480 TFT display
- Processes payments in real time via MQTT (HiveMQ)
- Dispenses product via servo-controlled spray nozzles (2 doses per payment)
- Syncs prices from server every 30 seconds
- Caches prices in NVS for offline operation
- 5-minute payment timeout with on-screen countdown
- Error handling: WiFi/MQTT disconnect screens, order cancellation
| Main Menu | Product Selected | Loading | QR Payment |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| 4 products with live prices | Highlight on button press | Connecting to server | Payme QR code on screen |
┌──────────────────────────────┐
│ Vending Machine │
│ │
│ ┌────────┐ ┌────────────┐ │ ┌──────────────┐
│ │ 4 Btns │ │ TFT 320x480│ │ HTTPS │ Flask Server │
│ │ Select │ │ LVGL UI │ │────────►│ │
│ └────────┘ │ QR Display │ │ │ /create-order│
│ └────────────┘ │◄──MQTT──│ /payme │
│ ┌────────┐ ┌────────────┐ │ │ /api/prices │
│ │ 4 Btns │ │ 4 Servos │ │ └──────┬───────┘
│ │ Spray │ │ Nozzles │ │ │
│ └────────┘ └────────────┘ │ ┌──────┴───────┐
│ │ │ Payme API │
│ ESP32 + WiFi + MQTT │ │ (Webhook) │
└──────────────────────────────┘ └──────────────┘
- Customer presses one of 4 buttons (Tom Ford / Lanvin / Dior / Dolce&Gabbana)
- Screen highlights the selected fragrance, shows loading animation (3 sec)
- ESP32 sends
POST /create-orderto the server with product ID and price - Server generates Payme checkout URL, publishes via MQTT
- ESP32 renders QR code on TFT display
- Customer scans QR with Payme app and pays
- Server receives Payme webhook → publishes
"confirmed"via MQTT - ESP32 shows success screen, grants 2 spray doses
- Customer presses the spray button to dispense (servo activates per press)
- After 2 doses or 5-min timeout → machine resets to idle
| Component | Qty | Purpose |
|---|---|---|
| ESP32 DevKit | 1 | Main controller |
| TFT ILI9488 320×480 | 1 | LVGL UI + QR display |
| Servo SG90 | 4 | Spray nozzle control |
| Push buttons | 9 | 4 select + 4 spray + 1 cancel |
| Perfume tanks + nozzles | 4 | Product storage |
Buttons (select): GPIO 22, 23, 32, 33
Buttons (spray): GPIO 19, 21, 26, 27
Button (cancel): GPIO 25
Servo: GPIO 18 (+ 3 more slots)
TFT: SPI (TFT_eSPI config)
| Module | File | Description |
|---|---|---|
| Main loop | main.cpp |
Setup, LVGL tick, state machine, timeouts |
| Payment | payment.cpp |
WiFi, MQTT, QR generation, order lifecycle, price sync |
| Buttons | buttons_control.cpp |
Product selection, cancel, spray trigger |
| Servos | Servo_controll.cpp |
Dose dispensing (servo rotate per button press) |
| Display | tft_draw.cpp |
LVGL flush callback for TFT_eSPI |
| Server | app.py |
Flask: Payme webhook, MQTT publisher, price API |
| Screen | Purpose |
|---|---|
| Screen1 | Main menu — 4 products with prices |
| Screen2 | QR code display — waiting for payment |
| Screen3 | Error — no MQTT connection |
| Screen4 | Loading animation |
| Screen5 | Payment success |
| Screen6 | Payment cancelled |
| Screen7 | Timeout countdown (9 sec) |
Street-Aroma-Vending/
├── firmware/
│ ├── main.cpp # Entry point, state machine
│ ├── payment.cpp # WiFi, MQTT, QR, orders, prices
│ ├── buttons_control.cpp # Button handling, product selection
│ ├── Servo_controll.cpp # Servo dispensing control
│ ├── tft_draw.cpp # LVGL display driver
│ └── Globals.h # Shared defines, pins, variables
│
├── server/
│ └── app.py # Flask: Payme webhook + MQTT + prices API
│
├── screenshots/
│ ├── main_menu.jpg # Main product selection screen
│ ├── product_selected.jpg # Highlighted product on button press
│ ├── loading.jpg # Loading spinner
│ └── qr_payment.jpg # Payme QR code screen
│
└── README.md
QR Code Generation on Device — Payme checkout URLs are rendered as QR codes directly on the ESP32 using RGB565 pixel buffer in PSRAM. No external QR service needed.
Remote Price Management — Prices are fetched from the server every 30 seconds and cached in ESP32 NVS. If the server is unreachable, cached prices are used.
Dose Counting — Each payment grants exactly 2 spray doses. A counter on screen shows remaining doses. The servo only activates when the spray button is pressed and doses remain.
Payment Timeout — If the customer doesn't pay within 5 minutes, a 9-second countdown appears. Any button press extends the timer. After countdown, the order is cancelled automatically.
Error Recovery — If MQTT disconnects, the machine shows an error screen and retries every 3 seconds. When connection is restored, it returns to the main menu.
The backend (app.py) handles:
- Payme JSON-RPC webhook — full transaction lifecycle (CheckPerform, Create, Perform, Cancel)
- MQTT publishing — sends
created/confirmed/cancelledto the ESP32 - Price management —
GET/POST /api/pricesfor remote updates - Order tracking — stores all orders in
orders.json
See Payme-QR-Payment-Terminal for the standalone payment server documentation.
pip install flask paho-mqtt python-dotenv
# Configure .env
MERCHANT_ID=your_merchant_id
PAYME_KEY=your_secret_key
MQTT_BROKER=broker.hivemq.com
python app.pyUpdate Globals.h:
#define WIFI_SSID "Your_WiFi"
#define WIFI_PASSWORD "Your_Password"
#define MQTT_SERVER "broker.hivemq.com"
#define MQTT_TOPIC "payments/your_merchant_id"
#define SERVER_URL "https://your-server.com/api"
#define DEVICE_ID "street-aroma-01"Flash with Arduino IDE (ESP32 board, PSRAM enabled).
Set webhook URL in Payme merchant dashboard:
https://your-server.com/payme
| Library | Purpose |
|---|---|
| TFT_eSPI | TFT display driver |
| LVGL 8.3 | UI framework |
| PubSubClient | MQTT client |
| ArduinoJson | JSON parsing |
| ESP32Servo | Servo control |
| qrcode | QR code generation |
| WiFiClientSecure | HTTPS requests |
| Preferences | NVS price caching |
Temur Eshmurodov — @myseringan
MIT License — free to use and modify.



