NTM Solutions

Thứ Sáu, 14 tháng 8, 2026

🥤 Bài 10 — Hoàn thiện máy bán nước mini: ESP32 + Laravel + QR + Motor + Sensor

Sau 9 bài, chúng ta đã lần lượt xây dựng:

  • Cơ cấu nhả lon.

  • Motor và driver.

  • ESP32.

  • Encoder/Sensor.

  • State Machine.

  • Màn hình cảm ứng.

  • Thanh toán QR.

  • Backend Laravel.

  • Quản lý tồn kho.

Bây giờ chúng ta ghép tất cả lại thành một hệ thống vending machine mini hoàn chỉnh.

Mục tiêu của bài này không phải làm một chiếc máy thương mại ngay lập tức.

Mục tiêu là:

Làm cho một người có thể chọn một lon nước → thanh toán → máy tự động nhả đúng một lon → cảm biến xác nhận → hệ thống cập nhật tồn kho.



1. Kiến trúc tổng thể

Hệ thống cuối cùng:

                    👤 KHÁCH HÀNG
                         │
                         ▼
                 ┌───────────────┐
                 │  TOUCH SCREEN │
                 └───────┬───────┘
                         │
                    Chọn sản phẩm
                         │
                         ▼
                 ┌───────────────┐
                 │     ESP32     │
                 └───────┬───────┘
                         │
                    HTTP / HTTPS
                         │
                         ▼
                 ┌───────────────┐
                 │    LARAVEL    │
                 │    SERVER     │
                 └───────┬───────┘
                         │
                    Tạo Order
                         │
                         ▼
                       QR
                         │
                         ▼
                    💳 Thanh toán
                         │
                         ▼
                 Payment = PAID
                         │
                         ▼
                      ESP32
                         │
                         ▼
                 ┌───────────────┐
                 │ MOTOR DRIVER  │
                 └───────┬───────┘
                         │
                         ▼
                       MOTOR
                         │
                         ▼
                     TRỤC XOẮN
                         │
                         ▼
                        🥤
                         │
                         ▼
                     SENSOR
                         │
                         ▼
                 Product Delivered
                         │
                         ▼
                      Laravel
                         │
                         ▼
                    Stock - 1

Đây chính là kiến trúc IoT vending machine cơ bản.


2. Phần cứng phiên bản mini

Để học, chúng ta không cần làm máy lớn.

Một phiên bản nhỏ có thể gồm:

ESP32
Màn hình cảm ứng
Motor DC hoặc Stepper
Motor Driver
IR Sensor
Encoder/Hall Sensor
Nguồn DC
Nút dừng khẩn cấp
Cơ cấu trục xoắn
Khung máy

Có thể bắt đầu với một ngăn duy nhất.

Ví dụ:

┌──────────────────────────┐
│                          │
│        🥤 🥤 🥤 🥤      │
│        🥤 🥤 🥤 🥤      │
│                          │
│        TRỤC XOẮN         │
│  ════════════════════    │
│                          │
│             ↓            │
│          SENSOR          │
│             ↓            │
│        ┌────────┐        │
│        │ NHẬN   │        │
│        │ HÀNG   │        │
│        └────────┘        │
└──────────────────────────┘

Sau khi chạy ổn định mới nhân lên:

A1
A2
B1
B2

3. Tách hệ thống thành 4 lớp

Đây là cách chúng ta tránh việc code trở thành một mớ hỗn độn.

Lớp 1 — UI

Touch Screen

Nhiệm vụ:

  • Hiển thị sản phẩm.

  • Nhận thao tác.

  • Hiển thị QR.

  • Hiển thị trạng thái.


Lớp 2 — Machine Controller

State Machine

Nhiệm vụ:

  • Quyết định máy đang ở trạng thái nào.

  • Chuyển trạng thái.

  • Kiểm soát timeout.

  • Xử lý lỗi.


Lớp 3 — Hardware

Motor
Sensor
Encoder

Nhiệm vụ:

  • Chạy motor.

  • Dừng motor.

  • Đọc sensor.

  • Phát hiện sản phẩm.


Lớp 4 — Backend

Laravel
Database
Payment
Inventory
Admin

Nhiệm vụ:

  • Quản lý sản phẩm.

  • Quản lý giá.

  • Tạo Order.

  • Xác nhận thanh toán.

  • Quản lý tồn kho.

  • Ghi log.

  • Quản lý máy.


4. State Machine hoàn chỉnh

Đây là phần quan trọng nhất.

                 ┌──────────┐
                 │   IDLE   │
                 └────┬─────┘
                      ↓
                 ┌──────────┐
                 │  SELECT  │
                 └────┬─────┘
                      ↓
              ┌───────────────┐
              │ CREATE_ORDER  │
              └───────┬───────┘
                      ↓
              ┌───────────────┐
              │    SHOW_QR    │
              └───────┬───────┘
                      ↓
             ┌─────────────────┐
             │  WAIT_PAYMENT   │
             └────────┬────────┘
                      │
              ┌───────┴───────┐
              ↓               ↓
          TIMEOUT            PAID
              ↓               ↓
             IDLE         DISPENSE
                              ↓
                           CHECK
                         ↙       ↘
                    SUCCESS      ERROR
                       ↓           ↓
                      IDLE       ERROR

Toàn bộ máy sẽ xoay quanh State Machine này.


5. Trạng thái IDLE

Khi không có người sử dụng:

IDLE

Màn hình:

┌──────────────────────────────┐
│                              │
│          🥤 DRINK            │
│                              │
│       MÁY BÁN NƯỚC          │
│                              │
│     Chạm để bắt đầu         │
│                              │
└──────────────────────────────┘

Không có giao dịch nào đang chạy.


6. Trạng thái SELECT

Khách chọn:

A1 — Coca

ESP32 lưu:

selectedSlot = "A1";

Nhưng chưa nhả hàng.


7. Kiểm tra tồn kho

ESP32 hỏi Backend:

GET /api/machines/VM001/products/A1

Server trả:

{
    "code": "A1",
    "name": "Coca",
    "price": 15000,
    "stock": 5,
    "status": "active"
}

Nếu:

stock > 0

thì tiếp tục.

Nếu:

stock = 0

hiển thị:

HẾT HÀNG

và quay lại:

SELECT

8. Tạo Order

Nếu còn hàng:

A1
Coca
15.000đ

ESP32 gọi:

POST /api/orders

Server tạo:

Order #10001

Trạng thái:

payment_status = pending
delivery_status = pending

9. Hiển thị QR

Server trả về thông tin giao dịch.

ESP32 hiển thị:

┌──────────────────────────────┐
│          COCA                │
│                              │
│         15.000đ              │
│                              │
│       ┌───────────┐          │
│       │           │          │
│       │    QR     │          │
│       │           │          │
│       └───────────┘          │
│                              │
│      Quét để thanh toán      │
│                              │
└──────────────────────────────┘

State:

WAIT_PAYMENT

10. Chờ thanh toán

ESP32 định kỳ hỏi:

GET /api/orders/10001

Server trả:

{
    "payment_status": "pending"
}

Tiếp tục chờ.

Khi:

{
    "payment_status": "paid"
}

ESP32 chuyển:

WAIT_PAYMENT
        ↓
DISPENSE

11. Timeout

Ví dụ:

120 giây

Nếu khách không thanh toán:

WAIT_PAYMENT
       ↓
TIMEOUT
       ↓
IDLE

Màn hình:

Giao dịch đã hết thời gian.

Vui lòng thử lại.

12. Bắt đầu nhả hàng

Khi thanh toán thành công:

DISPENSE

ESP32:

Motor ON

Motor quay:

↻
↻
↻

Trục xoắn đẩy lon về phía trước.


13. Encoder

Nếu dùng encoder, ESP32 có thể biết motor đã quay bao nhiêu.

Ví dụ:

Target = 1000 pulses

Motor chạy:

0
100
200
300
...
900
1000

Khi:

pulse >= target

thì:

motorStop();

14. Nhưng Encoder chưa đủ

Encoder chỉ cho biết:

Motor đã quay đúng vị trí.

Nó không chắc chắn:

Lon đã thực sự rơi xuống hay chưa.

Ví dụ:

Motor
  ↓
Trục xoắn quay
  ↓
Lon bị kẹt

Encoder vẫn có thể báo:

OK

Vì vậy cần thêm:

Sensor xác nhận sản phẩm.


15. Sensor xác nhận lon

Ví dụ sử dụng IR Sensor:

       🥤
        ↓
        ↓
   ┌─────────┐
   │ IR      │
   │ SENSOR  │
   └────┬────┘
        ↓
   PRODUCT_DETECTED

ESP32 chờ:

Sensor = DETECTED

16. Hai điều kiện để giao hàng thành công

Chúng ta cần:

Motor position = OK

AND

Product sensor = DETECTED

Khi cả hai đúng:

DELIVERY SUCCESS

17. Nếu sản phẩm không rơi

Ví dụ:

Motor → OK
Sensor → NO PRODUCT

Sau khoảng:

5 giây

máy chuyển:

ERROR

Màn hình:

┌──────────────────────────────┐
│             !                │
│                              │
│      KHÔNG THỂ GIAO HÀNG     │
│                              │
│      Vui lòng liên hệ        │
│      nhân viên hỗ trợ.       │
└──────────────────────────────┘

18. Không trừ kho khi lỗi

Đây là nguyên tắc rất quan trọng.

Nếu:

Payment = PAID
Delivery = FAILED

thì:

stock không được giảm

Ví dụ:

Stock = 5

Thanh toán
↓
Motor lỗi
↓
Sensor không phát hiện

Stock vẫn = 5

Nhưng Order phải được lưu:

payment_status = paid
delivery_status = failed

Để hệ thống xử lý tiếp.


19. Nếu giao thành công

Nếu:

Sensor = DETECTED

thì:

delivery_status = success

Sau đó:

stock = stock - 1

và ghi:

InventoryLog

Ví dụ:

A1
SALE
-1
Order #10001

20. Cập nhật UI

Màn hình:

┌──────────────────────────────┐
│                              │
│             ✓                │
│                              │
│       GIAO HÀNG THÀNH CÔNG  │
│                              │
│        Vui lòng lấy hàng     │
│                              │
└──────────────────────────────┘

Sau vài giây:

SUCCESS
   ↓
IDLE

21. Backend Laravel

Kiến trúc API tối thiểu:

/api
│
├── machines
│
├── products
│
├── orders
│
├── payments
│
└── inventory

Ví dụ:

POST /api/orders
GET  /api/orders/{id}

GET  /api/machines/{code}
GET  /api/machines/{code}/products

POST /api/machines/{code}/events

22. API Order

Ví dụ:

public function store(Request $request)
{
    $machine = Machine::where(
        'code',
        $request->machine_code
    )->firstOrFail();

    $machineProduct = MachineProduct::where(
        'machine_id',
        $machine->id
    )
    ->where('slot', $request->slot)
    ->firstOrFail();

    if ($machineProduct->stock <= 0) {
        return response()->json([
            'message' => 'Out of stock'
        ], 422);
    }

    $order = Order::create([
        'machine_id' => $machine->id,
        'machine_product_id' => $machineProduct->id,
        'amount' => $machineProduct->product->price,
        'payment_status' => 'pending',
        'delivery_status' => 'pending',
    ]);

    return response()->json($order);
}

Điểm quan trọng:

Giá lấy từ Database, không lấy từ ESP32.


23. API kiểm tra Order

public function show(Order $order)
{
    return response()->json([
        'id' => $order->id,
        'payment_status' => $order->payment_status,
        'delivery_status' => $order->delivery_status,
    ]);
}

ESP32 chỉ cần biết:

pending
paid
expired

và:

pending
dispensing
success
failed

24. API báo giao hàng

Sau khi cảm biến phát hiện lon:

POST /api/orders/10001/delivery

Dữ liệu:

{
    "status": "success"
}

Server xử lý:

Order
↓
delivery_status = success
↓
stock - 1
↓
InventoryLog

Tất cả nên nằm trong một transaction.


25. Một Order hoàn chỉnh

Ví dụ:

Order #10001

Machine:
VM001

Slot:
A1

Product:
Coca

Amount:
15.000đ

Payment:
PAID

Delivery:
SUCCESS

Created:
21:10:01

Paid:
21:10:14

Delivered:
21:10:17

Đây là dữ liệu rất có giá trị cho hệ thống quản lý sau này.


26. ESP32 cần những Module nào?

Không nên viết tất cả trong main.cpp.

Có thể chia:

esp32/
│
├── main.cpp
│
├── config.h
│
├── machine/
│   ├── state_machine.cpp
│   └── state_machine.h
│
├── motor/
│   ├── motor.cpp
│   └── motor.h
│
├── sensor/
│   ├── sensor.cpp
│   └── sensor.h
│
├── display/
│   ├── display.cpp
│   └── display.h
│
├── api/
│   ├── api.cpp
│   └── api.h
│
└── payment/
    ├── payment.cpp
    └── payment.h

Cấu trúc này sẽ giúp dự án dễ mở rộng.


27. main.cpp

Không nên có hàng nghìn dòng code.

Ý tưởng:

void setup()
{
    machine.begin();
    motor.begin();
    sensor.begin();
    display.begin();
    api.begin();
}

void loop()
{
    machine.update();
}

State Machine sẽ điều phối các thành phần còn lại.


28. State Machine

Ví dụ:

enum MachineState
{
    IDLE,
    SELECT,
    CREATE_ORDER,
    SHOW_QR,
    WAIT_PAYMENT,
    DISPENSE,
    CHECK,
    SUCCESS,
    ERROR
};

Biến:

MachineState state = IDLE;

29. Hàm update()

Ý tưởng:

void Machine::update()
{
    switch (state)
    {
        case IDLE:
            handleIdle();
            break;

        case SELECT:
            handleSelect();
            break;

        case CREATE_ORDER:
            handleCreateOrder();
            break;

        case WAIT_PAYMENT:
            handlePayment();
            break;

        case DISPENSE:
            handleDispense();
            break;

        case CHECK:
            handleCheck();
            break;

        case SUCCESS:
            handleSuccess();
            break;

        case ERROR:
            handleError();
            break;
    }
}

Đây là cách tốt hơn rất nhiều so với:

if (...)
{
}
else if (...)
{
}
else if (...)
{
}
else if (...)
{
}

hàng trăm lần trong loop().


30. Không dùng delay() cho toàn bộ máy

Không nên:

motorOn();

delay(5000);

motorOff();

Trong thời gian delay():

  • UI có thể không phản hồi.

  • Sensor không được xử lý tốt.

  • Network bị gián đoạn.

  • Payment không được kiểm tra.

Thay vào đó sử dụng:

millis()

hoặc một state machine không blocking.


31. Máy cần Internet

ESP32 có thể kết nối:

Wi-Fi

Luồng:

ESP32
 ↓
Wi-Fi Router
 ↓
Internet
 ↓
Laravel Server

Trong phiên bản mini đặt trên bàn học, Wi-Fi là lựa chọn đơn giản nhất.


32. Nếu mất Internet?

Đây là một lỗi phải tính đến.

Ví dụ:

Khách chọn Coca
       ↓
ESP32 mất Wi-Fi

Không được cho máy hoạt động bình thường như chưa có chuyện gì.

Màn hình:

┌──────────────────────────────┐
│                              │
│        ⚠ MẤT KẾT NỐI        │
│                              │
│    Vui lòng thử lại sau.     │
│                              │
└──────────────────────────────┘

33. Nếu mất mạng sau khi khách thanh toán?

Tình huống khó hơn:

Khách thanh toán
       ↓
Payment = PAID
       ↓
ESP32 mất Wi-Fi

Không được tự động tạo Order mới.

Order cũ vẫn tồn tại trên Server.

Khi ESP32 kết nối lại:

GET Order #10001

Server trả:

PAID

Máy tiếp tục:

DISPENSE

Đây là lý do Order ID phải được lưu trên ESP32 trong quá trình giao dịch.


34. Nút Emergency Stop

Một máy cơ khí thực tế nên có:

Nút dừng khẩn cấp.

Ví dụ:

        🔴
   EMERGENCY STOP

Khi nhấn:

Motor OFF

ngay lập tức.

Không chờ:

State Machine

xử lý xong.

Đây là một lớp bảo vệ phần cứng riêng.


35. Nguồn điện

Không cấp motor trực tiếp từ chân GPIO ESP32.

Sai:

ESP32 GPIO
    │
    ▼
  MOTOR

Đúng:

Power Supply
     │
     ├──────────► Motor Driver ───► Motor
     │
     └──────────► Voltage Regulator ───► ESP32

ESP32 chỉ gửi:

PWM
DIR
ENABLE

tới driver.


36. GND chung

Nếu dùng nguồn riêng cho motor và ESP32, cần thiết kế mass đúng cách.

Thông thường:

ESP32 GND
     │
     ├──────── Motor Driver GND
     │
     └──────── Sensor GND

Nhưng nguồn và driver phải được thiết kế phù hợp với dòng tải và nhiễu của motor.


37. Phiên bản prototype nên cực kỳ đơn giản

Đừng bắt đầu bằng:

20 sản phẩm
QR thật
Camera
4G
NFC
Cloud
Dashboard
AI

Chúng ta chỉ cần:

1 sản phẩm
1 motor
1 sensor
1 ESP32
1 màn hình
1 Backend
1 QR demo

Mục tiêu:

Một lon nước được bán thành công từ đầu đến cuối.

Sau khi chạy ổn mới mở rộng.


38. Prototype V1

┌───────────────────────┐
│                       │
│       🥤 COCA        │
│                       │
│      15.000đ          │
│                       │
│     [ MUA NGAY ]      │
│                       │
└──────────┬────────────┘
           │
           ▼
        Laravel
           │
           ▼
          QR
           │
           ▼
       Thanh toán
           │
           ▼
         ESP32
           │
           ▼
         Motor
           │
           ▼
          🥤
           │
           ▼
        Sensor
           │
           ▼
        SUCCESS

Nếu V1 chạy được:

Dự án đã thành công về mặt nguyên lý.


39. Prototype V2

Sau đó thêm:

A1
A2
B1
B2

Kiến trúc:

          ESP32
             │
   ┌─────────┼─────────┐
   ↓         ↓         ↓
Motor A    Motor B   Motor C
   │         │         │
Sensor A   Sensor B  Sensor C

40. Prototype V3

Thêm:

QR thật
Inventory
Admin
Multiple machines
Remote monitoring

Lúc này:

                 CLOUD
                   │
          ┌────────┴────────┐
          │                 │
        VM001              VM002
          │                 │
        ESP32              ESP32
          │                 │
       Vending            Vending

41. Dashboard cuối cùng

Laravel Admin có thể hiển thị:

┌──────────────────────────────────────────────┐
│              VENDING DASHBOARD              │
├──────────────────────────────────────────────┤
│                                              │
│  🟢 VM001       🟢 VM002       🔴 VM003     │
│  Online         Online         Offline       │
│                                              │
│  Doanh thu hôm nay: 1.250.000đ              │
│  Đơn hàng: 87                                │
│  Giao thành công: 84                        │
│  Lỗi giao hàng: 3                           │
│                                              │
├──────────────────────────────────────────────┤
│ LOW STOCK                                   │
│                                              │
│ VM001 / B1 / Sprite / Còn 1                 │
│ VM002 / A2 / Pepsi  / Còn 0                 │
└──────────────────────────────────────────────┘

Đây đã không còn đơn thuần là một Arduino project.

Nó trở thành:

Một hệ thống IoT có thiết bị + Backend + Database + Payment + Dashboard.


42. Toàn bộ hệ thống sau 10 bài

                         👤
                         │
                         ▼
                  TOUCH SCREEN
                         │
                         ▼
                     ESP32
                         │
          ┌──────────────┼──────────────┐
          │              │              │
          ▼              ▼              ▼
        Motor          Sensor         Wi-Fi
          │              │              │
          ▼              ▼              ▼
       Cơ khí          IR/Encoder    INTERNET
                                         │
                                         ▼
                                  ┌─────────────┐
                                  │   LARAVEL   │
                                  └──────┬──────┘
                                         │
                ┌────────────────────────┼──────────────────┐
                │                        │                  │
                ▼                        ▼                  ▼
             ORDERS                  PAYMENT             INVENTORY
                │                        │                  │
                └────────────────────────┼──────────────────┘
                                         │
                                         ▼
                                  ADMIN DASHBOARD

43. Thành quả sau Bài 10

Chúng ta đã đi từ:

Một motor

đến:

Một hệ thống vending machine hoàn chỉnh về nguyên lý.

Cụ thể:

Phần cơ khí

Khung
Trục xoắn
Motor
Driver
Cơ cấu nhận hàng

Phần điện tử

ESP32
Sensor
Encoder
Nguồn
Driver
Emergency Stop

Phần phần mềm

State Machine
UI
HTTP
API

Phần Backend

Laravel
Database
Order
Payment
Inventory
Admin

🧪 Bài thực hành cuối của phần cơ bản

Đừng làm ngay một máy 10–20 ngăn.

Hãy làm đúng một ngăn duy nhất.

Phần cứng

[ ] ESP32
[ ] Motor
[ ] Motor Driver
[ ] IR Sensor
[ ] Encoder/Hall Sensor
[ ] Màn hình
[ ] Nút Emergency Stop
[ ] Trục xoắn

Phần mềm

[ ] IDLE
[ ] SELECT
[ ] CREATE_ORDER
[ ] SHOW_QR
[ ] WAIT_PAYMENT
[ ] DISPENSE
[ ] CHECK
[ ] SUCCESS
[ ] ERROR

Backend

[ ] Products
[ ] Machines
[ ] Machine Products
[ ] Orders
[ ] Inventory Logs
[ ] Payment

Luồng kiểm thử

[ ] Chọn Coca
[ ] Kiểm tra tồn kho
[ ] Tạo Order
[ ] Hiển thị QR
[ ] Giả lập PAID
[ ] Motor quay
[ ] Sensor phát hiện lon
[ ] Ghi SUCCESS
[ ] Trừ stock
[ ] Ghi Inventory Log
[ ] Quay về IDLE

Nếu toàn bộ các bước này chạy ổn:

🏆 Bạn đã có một vending machine mini hoạt động đúng nguyên lý.


🎯 Tổng kết 10 bài đầu tiên

Chúng ta đã xây dựng dự án theo đúng hướng của một sản phẩm kỹ thuật:

Bài 1
↓
Ý tưởng vending machine mini

Bài 2
↓
Cấu trúc và nguyên lý cơ khí

Bài 3
↓
Cơ cấu nhả lon

Bài 4
↓
Motor + ESP32

Bài 5
↓
Cảm biến + phản hồi

Bài 6
↓
State Machine

Bài 7
↓
Touch UI

Bài 8
↓
QR Payment

Bài 9
↓
Inventory

Bài 10
↓
GHÉP TOÀN BỘ HỆ THỐNG

Và kiến trúc cuối cùng là:

              🥤 VENDING MACHINE
                       │
          ┌────────────┼────────────┐
          │            │            │
       MECHANICAL   ELECTRONIC   SOFTWARE
          │            │            │
       Motor         ESP32       State Machine
       Spiral        Sensor      Touch UI
       Frame         Driver      API
          │            │            │
          └────────────┼────────────┘
                       │
                    INTERNET
                       │
                       ▼
                    LARAVEL
                       │
          ┌────────────┼────────────┐
          │            │            │
        ORDER       PAYMENT      INVENTORY
          │            │            │
          └────────────┼────────────┘
                       │
                       ▼
                  ADMIN PANEL

Đây là mốc kết thúc phần "làm một vending machine mini chạy được".

Từ đây, phần tiếp theo có thể chuyển sang thiết kế cơ khí thực tế, PCB, nguồn, hộp máy, nhiều ngăn, làm lạnh, chống kẹt lon, giám sát từ xa và đưa máy từ prototype thành sản phẩm có thể chế tạo.

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

Đăng nhận xét

Facebook Youtube RSS