Ở các bài trước, máy của chúng ta đã có:
ESP32 điều khiển.
Motor và cơ cấu trục xoắn.
Encoder/Hall Sensor.
Cảm biến phát hiện sản phẩm.
State Machine.
Giao diện cảm ứng.
Bây giờ chúng ta thêm một thành phần rất quan trọng:
Thanh toán QR.
Mục tiêu cuối cùng là:
Khách chọn Coca
↓
Máy tạo giao dịch
↓
Hiển thị QR
↓
Khách quét QR
↓
Thanh toán thành công
↓
Server xác nhận
↓
ESP32 nhận lệnh
↓
Nhả Coca1. Không nên để ESP32 xử lý thanh toán trực tiếp
Đây là nguyên tắc quan trọng.
Không nên thiết kế:
QR
↓
ESP32
↓
Ngân hàng
ESP32 không nên trực tiếp xử lý:
thông tin tài khoản ngân hàng
thông tin thẻ
chữ ký giao dịch
khóa bí mật API
xác thực thanh toán
Thay vào đó, chúng ta cần một Backend Server.
Kiến trúc:
KHÁCH
│
▼
Quét mã QR
│
▼
Cổng thanh toán
│
▼
SERVER
│
▼
ESP32
│
▼
MOTOR
│
▼
🥤
2. Tại sao cần Server?
Giả sử Coca có giá:
15.000đ
ESP32 yêu cầu server tạo một giao dịch:
POST /api/orders
Dữ liệu:
{
"machine": "VM001",
"product": "A1",
"amount": 15000
}
Server tạo:
Order #10025
Sau đó trả về thông tin thanh toán.
3. Máy không nên tin vào QR
Một sai lầm rất nguy hiểm là:
Hiển thị QR
↓
Khách quét
↓
ESP32 tự cho rằng đã thanh toán
Không được.
Khách có thể:
chụp ảnh QR.
gửi ảnh cho người khác.
không thanh toán.
thanh toán sai số tiền.
Máy chỉ được nhả hàng khi:
Server xác nhận giao dịch hợp lệ.
4. Tạo đơn hàng
Khi khách chọn:
A1
Coca
15.000đ
ESP32 gửi:
machine = VM001
product = A1
amount = 15000
Server tạo một Order:
orders
id: 10025
machine: VM001
product: A1
amount: 15000
status: pending
Ban đầu:
pending
5. QR thanh toán
Server có thể tạo dữ liệu QR dựa trên giao dịch.
Ví dụ:
VietQR
Nội dung có thể chứa:
Ngân hàng
Số tài khoản
Số tiền
Nội dung chuyển khoản
Ví dụ nội dung:
VM001-10025
Khách chuyển:
15.000đ
với nội dung:
VM001-10025
Server sẽ dùng thông tin giao dịch để xác định tiền thuộc về đơn hàng nào.
6. Vì sao phải có mã giao dịch riêng?
Không nên dùng:
COCA
làm nội dung thanh toán.
Vì trong một ngày có thể có:
COCA
COCA
COCA
COCA
Thay vào đó:
VM001-10025
VM001-10026
VM001-10027
Mỗi giao dịch có một ID riêng.
7. State Machine mới
Ở bài 6 chúng ta có:
IDLE
↓
SELECT
↓
PAYMENT
↓
DISPENSE
↓
CHECK
↓
SUCCESS
Bây giờ PAYMENT chi tiết hơn:
SELECT
↓
CREATE_ORDER
↓
SHOW_QR
↓
WAIT_PAYMENT
↓
PAYMENT_SUCCESS
↓
DISPENSE
Nếu hết thời gian:
WAIT_PAYMENT
↓
TIMEOUT
↓
IDLE
8. Thời gian chờ thanh toán
Không thể để máy chờ vô hạn.
Ví dụ:
Thời gian thanh toán:
120 giây
Nếu khách không thanh toán:
PAYMENT TIMEOUT
Máy trở về:
IDLE
Đơn hàng:
pending
có thể được đánh dấu:
expired
9. Server nhận thông báo thanh toán
Có hai cách phổ biến.
Cách 1 — Webhook
Cổng thanh toán gửi thông báo cho server:
Payment Provider
↓
POST /api/payment/webhook
↓
Laravel
Server kiểm tra:
Giao dịch có tồn tại?
Số tiền có đúng?
Mã giao dịch có đúng?
Trạng thái có thành công?
Chữ ký có hợp lệ?
Nếu tất cả đúng:
status = paid
10. Cách 2 — ESP32 hỏi Server
Nếu hệ thống thanh toán hoặc kiến trúc của bạn không thuận tiện cho webhook trực tiếp đến thiết bị, ESP32 có thể hỏi server:
GET /api/orders/10025
Server trả:
{
"id": 10025,
"status": "paid"
}
ESP32:
pending
↓
pending
↓
pending
↓
paid
Khi nhận:
paid
thì:
DISPENSE
11. Webhook và Polling
| Phương pháp | Ưu điểm | Nhược điểm |
|---|---|---|
| Webhook | Nhanh, hiệu quả | Backend phải nhận callback |
| Polling | Dễ hiểu, dễ làm prototype | Tốn request |
| Kết hợp | Linh hoạt | Kiến trúc phức tạp hơn |
Đối với phiên bản học tập, chúng ta có thể bắt đầu bằng:
ESP32 polling Server
Sau khi hệ thống chạy ổn định mới chuyển sang kiến trúc nâng cao.
12. API Laravel
Vì chúng ta đang học Laravel, Backend có thể xây dựng bằng Laravel.
Ví dụ:
POST /api/orders
GET /api/orders/{order}
POST /api/payment/webhook
13. Tạo Order
Controller:
public function store(Request $request)
{
$order = Order::create([
'machine_code' => $request->machine,
'product_code' => $request->product,
'amount' => $request->amount,
'status' => 'pending',
]);
return response()->json([
'order_id' => $order->id,
'status' => $order->status,
]);
}
Sau này có thể bổ sung:
QR data.
Transaction ID.
Expired time.
Payment provider.
Signature.
14. Database
Một bảng orders đơn giản:
orders
id
machine_code
product_code
amount
status
transaction_code
paid_at
created_at
updated_at
Ví dụ:
10025
VM001
A1
15000
paid
VM001-10025
2026-08-09 12:05:32
15. Không tin dữ liệu từ ESP32
ESP32 gửi:
{
"product": "A1",
"amount": 15000
}
Nhưng server không nên tin tuyệt đối amount.
Ví dụ người dùng sửa request thành:
{
"product": "A1",
"amount": 100
}
Nếu server tin dữ liệu này thì máy có thể bị khai thác.
Server phải lấy giá từ database:
A1 → Coca → 15.000đ
Sau đó tự xác định:
amount = 15000
16. Luồng an toàn
Nên thiết kế:
ESP32
│
│ product=A1
▼
Laravel
│
│ tìm giá A1
▼
15.000đ
│
▼
Create Order
│
▼
Generate QR
│
▼
Customer Payment
│
▼
Payment Provider
│
▼
Webhook
│
▼
Laravel
│
▼
Order = PAID
│
▼
ESP32
│
▼
DISPENSE
17. ESP32 kiểm tra thanh toán
Ví dụ:
bool checkPayment(int orderId)
{
// Gửi HTTP request đến server
// Server trả về:
// pending
// paid
// expired
return false;
}
Trong State Machine:
case WAIT_PAYMENT:
if (checkPayment(orderId)) {
currentState = DISPENSE;
}
break;
18. Không dùng delay()
Không nên:
delay(5000);
rồi kiểm tra thanh toán.
Thay vào đó:
if (millis() - lastCheck >= 2000) {
lastCheck = millis();
checkPayment(orderId);
}
ESP32 có thể đồng thời:
cập nhật màn hình.
kiểm tra cảm ứng.
kiểm tra thanh toán.
xử lý cảm biến.
giám sát motor.
19. Trường hợp khách thanh toán nhưng máy lỗi
Đây là tình huống rất quan trọng.
Ví dụ:
Khách thanh toán 15.000đ
↓
Payment = PAID
↓
Motor chạy
↓
Lon bị kẹt
Không được chỉ hiện:
ERROR
và kết thúc.
Server phải ghi nhận:
payment = paid
delivery = failed
Sau đó hệ thống có thể:
thử nhả lại.
đánh dấu cần hoàn tiền.
thông báo nhân viên.
ghi log sự cố.
20. Trạng thái Order
Nên tách thanh toán và giao hàng.
Ví dụ:
payment_status
----------------
pending
paid
failed
expired
refunded
và:
delivery_status
----------------
pending
dispensing
success
failed
Như vậy:
PAID + DELIVERY_FAILED
không bị nhầm thành:
PAYMENT_FAILED
21. Một giao dịch hoàn chỉnh
Ví dụ khách mua A1:
12:10:01
SELECT A1
12:10:02
CREATE ORDER #10025
12:10:03
SHOW QR
12:10:15
PAYMENT PAID
12:10:16
DISPENSING
12:10:18
PRODUCT DETECTED
12:10:18
DELIVERY SUCCESS
Server có thể lưu toàn bộ lịch sử.
22. Dashboard quản trị
Vì Backend dùng Laravel, sau này chúng ta có thể tạo:
ADMIN DASHBOARD
Hiển thị:
Doanh thu hôm nay
------------------
1.250.000đ
Đơn hàng
------------------
87
Đã giao
------------------
84
Lỗi
------------------
3
Danh sách:
Order Machine Product Amount Status
10025 VM001 A1 15.000 SUCCESS
10026 VM001 B2 20.000 SUCCESS
10027 VM001 A1 15.000 ERROR
Đây chính là bước kết nối dự án máy bán nước với những kiến thức Laravel Admin CRUD mà chúng ta đã học trước đó.
23. Một máy hay nhiều máy
Ngay từ đầu nên có:
machine_code
Ví dụ:
VM001
VM002
VM003
Server sẽ biết:
Order #10025
Machine:
VM002
Sau này một hệ thống Laravel có thể quản lý:
SERVER
┌───────┼───────┐
↓ ↓ ↓
VM001 VM002 VM003
↓ ↓ ↓
ESP32 ESP32 ESP32
Đây chính là nền tảng để biến một mô hình học tập thành hệ thống IoT vending machine.
24. Bảo mật
Khi đưa máy ra Internet, không được để:
ESP32 → API
mà không xác thực.
Có thể dùng:
API Key
hoặc token riêng cho từng máy:
VM001
TOKEN_XXXXXXXX
Server kiểm tra token trước khi trả dữ liệu.
Ngoài ra:
Chỉ sử dụng HTTPS.
Không lưu secret thanh toán trong firmware nếu không cần.
Không cho ESP32 tự quyết định giao dịch đã thanh toán.
Kiểm tra số tiền ở server.
Kiểm tra mã giao dịch.
Chống xử lý một giao dịch hai lần.
25. Chống nhả hàng hai lần
Giả sử ESP32 hỏi:
Order #10025 = paid
và nhận được:
paid
ESP32 nhả hàng.
Nhưng do mạng chập chờn, ESP32 lại hỏi lần nữa:
Order #10025 = paid
Nếu chương trình không cẩn thận:
Motor
↓
Lon 1
Motor
↓
Lon 2
Để tránh điều này, Order cần có trạng thái giao hàng:
delivery_status = dispensing
Sau khi giao thành công:
delivery_status = success
ESP32 không được nhả lại Order đã có:
success
26. State Machine hoàn chỉnh
Sau bài 8:
┌──────────┐
│ IDLE │
└────┬─────┘
↓
┌──────────┐
│ SELECT │
└────┬─────┘
↓
┌──────────────┐
│ CREATE_ORDER │
└──────┬───────┘
↓
┌──────────────┐
│ SHOW_QR │
└──────┬───────┘
↓
┌────────────────┐
│ WAIT_PAYMENT │
└───────┬────────┘
│
┌─────────┴─────────┐
↓ ↓
TIMEOUT PAID
↓ ↓
IDLE DISPENSE
↓
CHECK
↙ ↘
SUCCESS ERROR
↓ ↓
IDLE SUPPORT
Đây đã là một luồng giao dịch hoàn chỉnh.
🧪 Bài thực hành
Chưa cần kết nối ngân hàng thật.
Hãy xây dựng trước một phiên bản giả lập thanh toán:
Bước 1
Khách chọn:
A1
Bước 2
Laravel tạo:
Order #10001
với:
status = pending
Bước 3
Màn hình ESP32 hiển thị:
15.000đ
[ QR DEMO ]
Bước 4
Tạo một nút trong Admin:
[ XÁC NHẬN THANH TOÁN ]
Bước 5
Admin bấm nút.
Database chuyển:
pending
↓
paid
Bước 6
ESP32 nhận:
paid
và chuyển:
WAIT_PAYMENT
↓
DISPENSE
Bước 7
Motor nhả sản phẩm.
Như vậy bạn có thể kiểm tra toàn bộ kiến trúc thanh toán mà chưa cần kết nối dịch vụ thanh toán thật.
🎯 Tổng kết
Sau bài 8, kiến trúc của máy đã tiến thêm một bước lớn:
INTERNET
│
▼
┌────────────┐
│ LARAVEL │
│ SERVER │
└─────┬──────┘
│
Payment API
│
▼
QR
│
▼
KHÁCH
│
Thanh toán
│
▼
┌────────────┐
│ LARAVEL │
└─────┬──────┘
│
PAID
│
▼
ESP32
│
▼
MOTOR
│
▼
🥤
Điểm quan trọng nhất của bài này là:
Thanh toán và nhả hàng phải là hai hệ thống riêng biệt, được kết nối bằng trạng thái giao dịch.
Đừng để ESP32 tự quyết định "đã thanh toán". Server phải xác nhận giao dịch, sau đó ESP32 mới được phép nhả sản phẩm.
🥤 Bài 9 — Quản lý kho và tự động báo hết hàng
Ở bài tiếp theo, chúng ta sẽ giải quyết một vấn đề rất thực tế:
Máy biết còn bao nhiêu lon?
Chúng ta sẽ xây dựng:
A1 → Coca → 5 lon
A2 → Pepsi → 2 lon
B1 → Sprite → 0 lon
Khi bán một lon:
5 → 4
Khi còn:
0
giao diện tự chuyển thành:
HẾT HÀNG
và hệ thống quản trị Laravel có thể biết máy nào đang cần nạp hàng.




Không có nhận xét nào:
Đăng nhận xét