NTM Solutions

Thứ Hai, 10 tháng 8, 2026

🥤 Bài 7 — Thiết kế giao diện cảm ứng cho máy bán nước

Ở bài 6, chúng ta đã xây dựng State Machine cho bộ não của máy.

Bây giờ đến phần mà khách hàng nhìn thấy đầu tiên:

Giao diện người dùng (UI/UX).

Một máy bán nước có phần cơ khí rất tốt nhưng giao diện khó sử dụng thì trải nghiệm vẫn rất tệ.

Mục tiêu của chúng ta là tạo một giao diện hiện đại, đơn giản, dễ bấm, lấy cảm hứng từ phong cách giao diện Windows/Material nhưng không sao chép logo hay nhận diện thương hiệu.


1. Màn hình của máy

Phiên bản mini có thể bắt đầu với:

  • Màn hình cảm ứng 3.5–4.3 inch.

  • ESP32.

  • Độ phân giải khoảng 480×272 hoặc 800×480.

  • Giao tiếp SPI hoặc RGB tùy loại màn hình.

Nếu muốn làm giao diện đẹp hơn, có thể chọn:

ESP32 + màn hình cảm ứng 4.3 inch + LVGL

LVGL là thư viện giao diện rất phù hợp cho các thiết bị nhúng.


2. Không thiết kế quá nhiều nút

Một lỗi phổ biến khi thiết kế máy bán hàng là đưa quá nhiều thông tin lên màn hình.

Ví dụ không nên:

┌───────────────────────────────┐
│ A1  Coca 10.000 │ A2 Pepsi... │
│ B1  Sprite ...  │ B2 Fanta... │
│ C1  Water ...   │ C2 Tea ...  │
│                               │
│ QR │ CASH │ CARD │ SETTINGS   │
└───────────────────────────────┘

Khách nhìn vào sẽ mất thời gian.

Chúng ta nên thiết kế:

┌───────────────────────────────┐
│        CHỌN ĐỒ UỐNG           │
│                               │
│ ┌─────────┐ ┌─────────┐       │
│ │  🥤     │ │  🥤     │       │
│ │ Coca    │ │ Pepsi   │       │
│ │ 15.000đ │ │ 15.000đ │       │
│ └─────────┘ └─────────┘       │
│                               │
│ ┌─────────┐ ┌─────────┐       │
│ │  🥤     │ │  🥤     │       │
│ │ Sprite  │ │ Fanta   │       │
│ │ 15.000đ │ │ 15.000đ │       │
│ └─────────┘ └─────────┘       │
└───────────────────────────────┘

Khách chỉ cần:

Nhìn → chọn → thanh toán → nhận hàng.


3. Thiết kế theo State Machine

Điểm quan trọng là:

UI không được tách rời State Machine.

Mỗi trạng thái của máy tương ứng với một màn hình.

IDLE
 ↓
Màn hình chờ

SELECT
 ↓
Màn hình chọn sản phẩm

PAYMENT
 ↓
Màn hình thanh toán

DISPENSE
 ↓
Màn hình đang lấy hàng

CHECK
 ↓
Màn hình kiểm tra

SUCCESS
 ↓
Màn hình hoàn tất

ERROR
 ↓
Màn hình báo lỗi

Như vậy phần mềm rất dễ quản lý.


4. Màn hình chờ — IDLE

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

┌──────────────────────────────┐
│                              │
│          VENDING             │
│                              │
│       🥤  DRINK BAR          │
│                              │
│     Chạm để chọn sản phẩm    │
│                              │
│             10:30            │
└──────────────────────────────┘

Có thể thêm:

  • Logo máy.

  • Hình ảnh sản phẩm.

  • Nhiệt độ.

  • Khuyến mãi.

  • Wi-Fi/4G.

  • Đồng hồ.

Nhưng không nên nhồi quá nhiều thông tin.


5. Màn hình chọn sản phẩm

Đây là màn hình quan trọng nhất.

Ví dụ:

┌──────────────────────────────┐
│ Chọn sản phẩm                │
│                              │
│ ┌────────┐  ┌────────┐       │
│ │ Coca   │  │ Pepsi  │       │
│ │ 15.000 │  │ 15.000 │       │
│ │   A1   │  │   A2   │       │
│ └────────┘  └────────┘       │
│                              │
│ ┌────────┐  ┌────────┐       │
│ │ Sprite │  │ Fanta  │       │
│ │ 15.000 │  │ 15.000 │       │
│ │   B1   │  │   B2   │       │
│ └────────┘  └────────┘       │
└──────────────────────────────┘

Mỗi sản phẩm nên hiển thị:

  • Hình ảnh.

  • Tên.

  • Giá.

  • Mã sản phẩm.

  • Trạng thái còn hàng.


6. Hết hàng

Không nên để khách bấm vào sản phẩm rồi mới báo hết hàng.

Ngay trên UI:

┌───────────────┐
│               │
│    Coca       │
│               │
│   HẾT HÀNG    │
│               │
└───────────────┘

Nút phải chuyển sang trạng thái:

DISABLED

Không thể nhấn.


7. Chọn sản phẩm

Khi khách chọn Coca:

┌──────────────────────────────┐
│         COCA                 │
│                              │
│            🥤                │
│                              │
│         15.000đ              │
│                              │
│     [   MUA NGAY   ]         │
│                              │
│     [   QUAY LẠI   ]         │
└──────────────────────────────┘

Không nên bắt khách đi qua quá nhiều màn hình.

Mục tiêu:

Từ màn hình chính đến thanh toán trong 1–2 thao tác.


8. Màn hình thanh toán

Ví dụ:

┌──────────────────────────────┐
│          THANH TOÁN          │
│                              │
│          Coca                │
│         15.000đ              │
│                              │
│     ┌───────────────┐        │
│     │      QR       │        │
│     │    ██████     │        │
│     │    ██  ██     │        │
│     └───────────────┘        │
│                              │
│     Quét mã để thanh toán    │
│                              │
│       [ HỦY GIAO DỊCH ]      │
└──────────────────────────────┘

Sau này có thể thêm:

QR

NFC

Thẻ

Tiền mặt

Nhưng bản mini đầu tiên chỉ cần QR là đủ.


9. Thanh toán thành công

Không chuyển ngay sang màn hình khác.

Nên cho khách thấy:

┌──────────────────────────────┐
│                              │
│             ✓                │
│                              │
│      THANH TOÁN THÀNH CÔNG  │
│                              │
│           15.000đ            │
│                              │
└──────────────────────────────┘

Sau khoảng thời gian rất ngắn:

PAYMENT
   ↓
DISPENSE

10. Màn hình đang nhả hàng

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

Không nên để màn hình đứng yên.

┌──────────────────────────────┐
│                              │
│        ĐANG LẤY HÀNG         │
│                              │
│             ↻                │
│                              │
│       Vui lòng chờ...        │
│                              │
└──────────────────────────────┘

Có thể hiển thị animation:

Motor
  ↻
  ↻
  ↻

Trong lúc đó ESP32 thực hiện:

Motor ON
    ↓
Encoder đếm
    ↓
Đủ vị trí
    ↓
Motor OFF

11. Cảm biến xác nhận

Sau khi motor dừng:

DISPENSE

↓

CHECK

↓

IR Sensor

Nếu cảm biến phát hiện lon:

SUCCESS

Nếu không:

ERROR

UI lúc này có thể hiển thị:

✓ Đã giao sản phẩm

Vui lòng lấy hàng.

12. Màn hình lỗi

Không nên sử dụng những thông báo kỹ thuật như:

ERROR CODE: MOTOR_04

Khách hàng không hiểu.

Thay bằng:

┌──────────────────────────────┐
│             !                │
│                              │
│      KHÔNG THỂ LẤY HÀNG      │
│                              │
│  Vui lòng thử lại hoặc liên  │
│  hệ nhân viên hỗ trợ.        │
│                              │
│       [ TRỞ VỀ ]             │
└──────────────────────────────┘

Mã lỗi kỹ thuật chỉ dành cho màn hình quản trị.


13. Màu sắc

Với concept máy mà chúng ta đang thiết kế, có thể sử dụng:

Background
#F5F7FA

Primary
#2563EB

Success
#16A34A

Warning
#F59E0B

Danger
#DC2626

Text
#111827

Không cần sử dụng quá nhiều màu.

Quy tắc đơn giản:

1 màu chính + 1 màu cảnh báo + 1 màu thành công.


14. Thiết kế nút bấm

Nút cảm ứng phải đủ lớn.

Không nên:

[ OK ]

quá nhỏ.

Nên:

┌──────────────────────┐
│      MUA NGAY        │
└──────────────────────┘

Khách đứng trước máy có thể:

  • tay ướt

  • ánh sáng mạnh

  • thao tác nhanh

Vì vậy vùng cảm ứng phải đủ rộng.


15. Không nên dùng quá nhiều animation

Animation chỉ nên giúp khách hiểu:

Đang xử lý
Đang thanh toán
Đang nhả hàng
Hoàn tất

Không nên:

  • hiệu ứng bay quá nhiều

  • chuyển cảnh quá lâu

  • animation nặng

  • video nền

Máy bán hàng cần:

Nhanh hơn là đẹp.


16. Kiến trúc UI

Chúng ta có thể tổ chức phần mềm:

UI
│
├── HomeScreen
├── ProductScreen
├── PaymentScreen
├── DispenseScreen
├── SuccessScreen
└── ErrorScreen

Trong khi phần điều khiển:

Core
│
├── VendingMachine
├── MotorController
├── SensorController
├── PaymentController
└── Inventory

Hai phần này giao tiếp với nhau.

              UI
               │
               ▼
        State Machine
               │
      ┌────────┼────────┐
      ▼        ▼        ▼
    Motor    Sensor   Payment

Đây là cách tổ chức tốt hơn rất nhiều so với việc nhét toàn bộ chương trình vào loop().


17. Dùng LVGL

Ở phiên bản đầu tiên, chúng ta có thể dùng LVGL để xây dựng UI.

Kiến trúc:

ESP32
 │
 ├── LVGL
 │     │
 │     ├── Button
 │     ├── Label
 │     ├── Image
 │     ├── Screen
 │     └── Animation
 │
 ├── Touch Driver
 │
 └── Vending Controller

LVGL xử lý giao diện.

ESP32 xử lý logic máy.


18. Ví dụ tạo nút

Một ví dụ đơn giản với LVGL:

lv_obj_t *btn;

btn = lv_button_create(lv_screen_active());

lv_obj_set_size(btn, 200, 70);
lv_obj_center(btn);

lv_obj_t *label = lv_label_create(btn);

lv_label_set_text(label, "MUA NGAY");
lv_obj_center(label);

Khi khách chạm vào nút, chương trình có thể chuyển State Machine:

currentState = SELECT;

Hoặc nếu đang ở màn hình chọn sản phẩm:

selectedProduct = 0;
currentState = PAYMENT;

19. UI không được điều khiển trực tiếp Motor

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

Không nên:

Button

↓

Motor

Mà phải:

Button
  ↓
UI Event
  ↓
State Machine
  ↓
Motor Controller
  ↓
Motor

Như vậy sau này:

  • màn hình bị thay đổi

  • thêm nút vật lý

  • thêm app điện thoại

  • thêm Web Admin

thì Motor Controller vẫn giữ nguyên.


20. Thiết kế hoàn chỉnh

Đến đây, hệ thống của chúng ta đã có kiến trúc:

                 TOUCH SCREEN
                       │
                       ▼
                    UI/LVGL
                       │
                       ▼
                STATE MACHINE
                       │
        ┌──────────────┼──────────────┐
        │              │              │
        ▼              ▼              ▼
     PAYMENT         MOTOR          SENSOR
        │              │              │
        ▼              ▼              ▼
      QR/API        DRIVER          IR/ENCODER
                       │
                       ▼
                     MOTOR
                       │
                       ▼
                    SPIRAL
                       │
                       ▼
                     🥤

Đây chính là lúc dự án bắt đầu giống một sản phẩm thực tế, thay vì chỉ là một mạch ESP32 điều khiển motor.


🧪 Bài thực hành

Trong bài này chưa cần làm toàn bộ giao diện.

Hãy làm theo thứ tự:

Bước 1

Tạo màn hình:

CHỌN SẢN PHẨM

Bước 2

Tạo 4 nút:

A1    A2

B1    B2

Bước 3

Khi nhấn A1:

selectedProduct = A1;

Bước 4

Chuyển sang:

PAYMENT

Bước 5

Giả lập thanh toán thành công bằng một nút:

THANH TOÁN

Bước 6

Chuyển:

PAYMENT
   ↓
DISPENSE

Sau đó mới kết nối với motor thật.


🎯 Tổng kết

Sau bài 7, chúng ta đã thiết kế được giao diện người dùng hoàn chỉnh về mặt luồng:

┌──────────┐
│  IDLE    │
└────┬─────┘
     ↓
┌──────────┐
│ SELECT   │
└────┬─────┘
     ↓
┌──────────┐
│ PAYMENT  │
└────┬─────┘
     ↓
┌──────────┐
│ DISPENSE │
└────┬─────┘
     ↓
┌──────────┐
│  CHECK   │
└────┬─────┘
     ↓
┌──────────┐
│ SUCCESS  │
└────┬─────┘
     ↓
    IDLE

Quan trọng nhất là chúng ta đã thống nhất một nguyên tắc:

UI chỉ nhận thao tác của người dùng. State Machine quyết định máy phải làm gì. Controller điều khiển phần cứng.

Đây sẽ là nền tảng để các bài sau không biến thành một đống code Arduino khó bảo trì.

Bài 8 — Kết nối thanh toán QR

Ở bài tiếp theo, chúng ta sẽ đưa thanh toán thật vào máy: tạo mã QR theo sản phẩm, nhận trạng thái thanh toán qua Internet, xác nhận giao dịch rồi mới cho phép ESP32 kích hoạt cơ cấu nhả lon.

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

Đăng nhận xét

Facebook Youtube RSS