Tự Học IoT Bài 04: Tự Làm Máy Đếm Sản Phẩm Với ESP32 & Cảm Biến Hồng Ngoại (Phần 1)

Hô biến các cảm biến rẻ tiền thành dây chuyền đếm sản phẩm. Hướng dẫn chi tiết cách dùng ESP32, cảm biến hồng ngoại và màn hình LCD cho dự án IoT siêu cuốn!

  • esp32
  • iot
  • cam-bien-hong-ngoai
  • lcd-i2c
  • tu-hoc-iot

Giới thiệu: Đưa "dây chuyền nhà máy" lên bàn làm việc của bạn!

Bạn đã bao giờ tò mò các nhà máy đếm hàng ngàn sản phẩm chạy qua băng chuyền mỗi ngày như thế nào chưa? Hôm nay, trong series "Sinh viên nghèo tự học IoT", chúng ta sẽ tự tay chế tạo một hệ thống đếm sản phẩm tự động, hiển thị trực tiếp lên màn hình LCD ở quy mô dự án cá nhân.

Và tuyệt vời nhất? Bạn hoàn toàn có thể làm điều này với mức chi phí "trà đá" ngay tại nhà! Ở phần 1 này, chúng ta sẽ xử lý phần cứng và code offline trước khi đưa nó "lên mây" (IoT) ở phần tiếp theo nhé.

🛠️ "Đồ nghề" cần chuẩn bị

Để bắt đầu, bạn cần chuẩn bị một "giỏ hàng" linh kiện cực kỳ cơ bản. Đừng lo, chúng rất rẻ và dễ tìm:

  • ESP32 Dev Module (Loại 30 chân - "Bộ não" của toàn hệ thống).
  • Cảm biến hồng ngoại (IR Sensor) (Mắt thần phát hiện vật thể).
  • Màn hình LCD 2004 (Kèm sẵn module giao tiếp I2C - Hiển thị 20 cột, 4 hàng rộng rãi).
  • Dây cắm Jumper (Loại đực-cái để kết nối các mạch với nhau).

🔌 Sơ đồ nối dây (Nhanh - Gọn - Lẹ)

Giao tiếp I2C của LCD giúp chúng ta tiết kiệm được vô số dây nhợ. Hãy cắm dây theo bảng dưới đây nhé:

1. Kết nối Cảm biến hồng ngoại (IR) với ESP32:

Cảm biến IRESP32
VCCVIN
GNDGND
OUTGPIO 23

2. Kết nối Màn hình LCD (I2C) với ESP32:

LCD I2CESP32
VCCVIN (5V)
GNDGND
SDAGPIO 21
SCLGPIO 22

Lưu ý xương máu: Hãy chú ý chỉnh biến trở màu xanh phía sau module I2C của màn hình LCD (dùng tua vít nhỏ) nếu bạn thấy màn hình sáng lên nhưng không hiện chữ nhé. Đó là nút chỉnh độ tương phản đấy!

💻 Cài đặt & Code mẫu

1. Chuẩn bị phần mềm

  • Mở Arduino IDE (Nếu chưa có, hãy lên Google gõ "Tải và cài đặt Arduino IDE").
  • Cài đặt Board ESP32 cho Arduino IDE.
  • Mở Library Manager (Quản lý thư viện), tìm từ khóa LiquidCrystal I2C và cài đặt thư viện của tác giả Frank de Brabander.

2. Code Đếm Sản Phẩm

Copy đoạn code "mì ăn liền" nhưng cực kỳ hiệu quả này vào Arduino IDE, cắm cáp và bấm Upload:

/*
  ESP32 DEV MODULE (30 PIN) - IR PRODUCT COUNTER + LCD 20x4 I2C + DTG IOT PLATFORM

  Thu vien cai tu Arduino IDE > Library Manager:
    LiquidCrystal I2C - Frank de Brabander
    Wire.h va Arduino.h co san trong ESP32 board package.

  CHAN CUNG MOT BEN, LIEN TIEP NHAU TREN ESP32 DEVKIT V1 30 PIN:
    GPIO18 -> SDA (LCD I2C)
    GPIO19 -> SCL (LCD I2C)
    GPIO21 <- OUT/DO (IR obstacle sensor)

  Cam bien IR: cap nguon 5V.
  LCD 20x4: Cap nguon 5V
  cho SDA/SCL. Noi GND chung tat ca thiet bi.
*/

#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <dtg.h>

// ====================== CAU HINH ======================
constexpr uint8_t PIN_LCD_SDA = 18;
constexpr uint8_t PIN_LCD_SCL = 19;
constexpr uint8_t PIN_IR      = 21;

constexpr uint8_t LCD_ADDRESS = 0x27; // Neu khong hien thi, thu 0x3F
constexpr uint8_t LCD_COLS    = 20;
constexpr uint8_t LCD_ROWS    = 4;

constexpr uint8_t IR_ACTIVE_LEVEL = LOW; // Da so module IR: LOW = co vat
constexpr uint32_t FILTER_MS      = 15;  // Loc nhieu (ms); chinh theo toc do bang tai

LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLS, LCD_ROWS);

// 1) Wi-Fi credentials
// 1) Wi-Fi credentials
static const char *WIFI_SSID    = "your-wifi-ssid";
static const char *WIFI_PASS    = "your-wifi-password";

// 2) Hub project + gateway credentials
static const char *MQTT_HOST    = "your-mqtt-host";
static const uint16_t MQTT_PORT = 8883; 
static const char *PROJECT_ID   = "your-project-id";
static const char *GW_ALIAS     = "your-gateway-alias";
static const char *MQTT_USER    = "your-mqtt-username";
static const char *MQTT_PASS    = "your-mqtt-password";
static const char *CLIENT_ID    = "your-client-id";

static const bool ENABLE_DEBUG  = true;

uint32_t productCount = 0;
bool rawDetected = false;
bool stableDetected = false;
uint32_t lastRawChangeAt = 0;
const int BUTTON_PIN = 0;

// ================= BIEN TRANG THAI IOT =================
bool isIotConnected = false;
bool lastIotState   = false;

DtgWiFi dtg(PROJECT_ID, GW_ALIAS, MQTT_USER, MQTT_PASS, CLIENT_ID);

DTG_CONNECTED() {
  dtgSerialLogConnected(dtg, CLIENT_ID);
}

// In 1 dong du 20 ky tu, khong su dung lcd.clear() lien tuc (tranh nhap nhay)
void printLCDLine(uint8_t row, const char *text) {
  lcd.setCursor(0, row);
  uint8_t i = 0;
  while (i < LCD_COLS && text[i] != '\0') {
    lcd.write((uint8_t)text[i]);
    ++i;
  }
  while (i < LCD_COLS) {
    lcd.write(' ');
    ++i;
  }
}

void showCount() {
  char line[21];
  snprintf(line, sizeof(line), "SO LUONG: %lu", (unsigned long)productCount);
  printLCDLine(1, line);
}

void showSensor() {
  printLCDLine(2, stableDetected ? "IR: CO SAN PHAM" : "IR: SAN SANG");
}

void updateIotStatus(bool connected) {
  if (connected) {
    printLCDLine(3, "IOT: DA KET NOI");
  } else {
    printLCDLine(3, "IOT: CHUA KET NOI");
  }
}

// Ham goi khi dem them 1 san pham
void onProductCounted() {
  ++productCount;
  if (dtg.connected()) {
    // Gui du lieu len IoT platform
    dtg.publish("sl_sanpham", productCount);
  }
  showCount();
  Serial.printf("DA DEM: %lu san pham\n", (unsigned long)productCount);
}

void scanIR() {
  const uint32_t now = millis();
  const bool detectedNow = (digitalRead(PIN_IR) == IR_ACTIVE_LEVEL);

  if (detectedNow != rawDetected) {
    rawDetected = detectedNow;
    lastRawChangeAt = now;
  }

  // Chi cong nhan thay doi neu trang thai lien tuc on dinh du FILTER_MS
  if (rawDetected != stableDetected && (uint32_t)(now - lastRawChangeAt) >= FILTER_MS) {
    stableDetected = rawDetected;
    showSensor();

    if (stableDetected) {
      onProductCounted(); // Dem tai canh bat dau phat hien san pham
    }
  }
}

void scanSerialReset() {
  while (Serial.available() > 0) {
    const char cmd = (char)Serial.read();
    if (cmd == 'R' || cmd == 'r') {
      productCount = 0;
      showCount();
      Serial.println("DA RESET BO DEM VE 0");
    }
  }
}

// Kiem tra va cap nhat trang thai ket noi IoT len LCD
void checkIotConnection() {
  isIotConnected = dtg.connected();

  // Chi cap nhat lai LCD khi trang thai thay doi de tranh giat lag
  if (isIotConnected != lastIotState) {
    lastIotState = isIotConnected;
    updateIotStatus(isIotConnected);
    Serial.printf("TRANG THAI IOT THAY DOI: %s\n", isIotConnected ? "CONNECTED" : "DISCONNECTED");
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_IR, INPUT_PULLUP);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // Doc trang thai ban dau
  rawDetected = (digitalRead(PIN_IR) == IR_ACTIVE_LEVEL);
  stableDetected = rawDetected;
  lastRawChangeAt = millis();

  // Khoi tao I2C va LCD
  Wire.begin(PIN_LCD_SDA, PIN_LCD_SCL);
  Wire.setClock(100000);

  lcd.init();
  lcd.backlight();
  lcd.clear();

  // Khoi tao hien thi ban dau
  printLCDLine(0, "DEM SAN PHAM - ESP32");
  showCount();
  showSensor();
  updateIotStatus(false); // Khoi dau hien "IOT: CHUA KET NOI"

  Serial.println("ESP32 IR COUNTER READY");
  Serial.println("GPIO18=SDA, GPIO19=SCL, GPIO21=IR OUT");
  Serial.println("Nhap R tren Serial Monitor de reset so dem.");

  // Log va ket noi DTG IoT
  dtg.setLogStream(&Serial);
  dtg.begin(WIFI_SSID, WIFI_PASS);
}

void loop() {
  scanIR();
  scanSerialReset();
  dtg.run();
  checkIotConnection(); // Kiem tra va cap nhat LCD khi co thay doi ket noi
}

🚀 Kết quả và Hẹn gặp lại!

Tada! Bây giờ hãy thử đưa tay qua cảm biến, bạn sẽ thấy số lượng trên màn hình LCD lập tức nhảy lên 1, 2, 3... .

Đây mới chỉ là bước khởi động. Ở Phần 2, chúng ta sẽ đẩy dữ liệu đếm được này lên App điện thoại và Dashboard Web để có thể giám sát nhà máy của bạn từ bất cứ đâu trên thế giới (chuẩn bài IoT luôn).

Nhớ chuẩn bị sẵn linh kiện và theo dõi bài viết tiếp theo nhé! Chúc các bạn "vọc" thành công!
Danh sách linh kiện mình để đây cho anh em lựa chọn