Bỏ qua, đến nội dung chính
Bài viếtFirmwareMới

ESP32 Beginner Guide – P6: Bluetooth Low Energy

Làm quen ESP32 BLE, GATT service và characteristic qua project gửi dữ liệu BME280 tới điện thoại bằng Read và Notify.

Chia sẻ

LinkedInFacebookX
An ESP32-S3 development board and smartphone testing Bluetooth Low Energy beside a MacBook Pro on a realistic electronics workbench

P5: Wi-Fi và Web Server, mình và bạn đã xem dữ liệu BME280 qua router. P6 sẽ tạo một đường khác: điện thoại kết nối trực tiếp với ESP32 bằng Bluetooth Low Energy, không cần biết SSID, địa chỉ IP hay có Internet.

BME280 → ESP32 → BLE → điện thoại

Mục tiêu là một GATT server nhỏ, dễ quan sát. Điện thoại có thể đọc giá trị hiện tại hoặc đăng ký để nhận giá trị mới mỗi hai giây.

BLE không phải Bluetooth Serial

ESP32 đời đầu hỗ trợ cả Bluetooth Classic và Bluetooth Low Energy. ESP32-S3 và ESP32-C3 trong series này hỗ trợ BLE nhưng không hỗ trợ Bluetooth Classic, vì vậy ví dụ BluetoothSerial không phù hợp với chúng.

BLE được thiết kế cho những lần trao đổi dữ liệu ngắn như cảm biến, trạng thái thiết bị và cấu hình gần thiết bị. Chữ “Low Energy” không đảm bảo mạch tự nhiên sẽ tiết kiệm pin: thời gian advertising, chu kỳ kết nối, cảm biến, LED và cách firmware ngủ vẫn quyết định dòng tiêu thụ thực tế.

Bốn khái niệm đủ để bắt đầu

Điện thoại quét thiết bị BLE Nivotek-BME280 phát từ ESP32-S3 DevKitC-1 bên cạnh MacBook Pro.

ESP32 quảng bá như peripheral; điện thoại đóng vai central để tìm và mở kết nối.

Khái niệmVai trò trong bài
PeripheralESP32 phát advertising và chờ kết nối
CentralĐiện thoại scan rồi kết nối tới ESP32
GATT serverBảng dữ liệu nằm trên ESP32
GATT clientỨng dụng trên điện thoại khám phá và dùng bảng đó

Advertising chỉ nói “thiết bị này đang ở gần và có service này”. Nó không phải luồng dữ liệu cảm biến liên tục. Sau khi kết nối, client khám phá cấu trúc GATT:

Nivotek sensor service
└── Measurement characteristic
    ├── Read
    └── Notify

Service gom một nhóm chức năng. Characteristic giữ một giá trị và các thao tác được phép. UUID là định danh để ứng dụng tìm đúng service hoặc characteristic, kể cả khi giao diện chỉ hiện một chuỗi số dài.

Chuẩn bị mạch và ứng dụng kiểm tra

Giữ mạch từ P4: BME280 nối 3V3, GND, SDA vào GPIO4, SCL vào GPIO5. Code dùng địa chỉ 0x77; hãy thay bằng 0x76 nếu I2C scanner của module bạn trả về địa chỉ đó.

Bạn cần:

  • ESP32-S3 DevKitC-1 và cáp USB-C truyền được dữ liệu.
  • BME280 đã chạy được với sketch P4.
  • Thư viện Adafruit BME280 Library và dependency Adafruit Unified Sensor.
  • Một GATT client trên điện thoại, chẳng hạn nRF Connect for Mobile.

Các header BLE dùng bên dưới đã đi cùng ESP32 core trong Arduino IDE; không cần cài một thư viện BLE trùng tên từ Library Manager.

Sketch hoàn chỉnh: gửi BME280 bằng Read và Notify

Ví dụ này tạo một custom service và một characteristic. Payload có dạng T=26.4,H=61,P=1009, cố ý giữ tối đa khoảng 20 byte để dễ thử với MTU mặc định. Đây là format học tập, chưa phải protocol cho sản phẩm.

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

constexpr uint8_t SDA_PIN = 4;
constexpr uint8_t SCL_PIN = 5;
constexpr uint8_t BME280_ADDRESS = 0x77;
constexpr uint32_t SAMPLE_INTERVAL_MS = 2000;

constexpr char DEVICE_NAME[] = "Nivotek-BME280";
constexpr char SERVICE_UUID[] =
    "8f7e1201-6a2d-4f0b-9c31-5d2a7e991001";
constexpr char MEASUREMENT_UUID[] =
    "8f7e1202-6a2d-4f0b-9c31-5d2a7e991001";

Adafruit_BME280 bme;
BLEServer *bleServer = nullptr;
BLECharacteristic *measurement = nullptr;

volatile bool deviceConnected = false;
bool wasConnected = false;
bool sensorReady = false;
uint32_t lastSampleAt = 0;

class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer *) override {
    deviceConnected = true;
  }

  void onDisconnect(BLEServer *) override {
    deviceConnected = false;
  }
};

void publishReading() {
  if (!sensorReady) {
    measurement->setValue("sensor unavailable");
    return;
  }

  const float temperature = bme.readTemperature();
  const float humidity = bme.readHumidity();
  const float pressure = bme.readPressure() / 100.0F;

  char payload[24];
  snprintf(payload, sizeof(payload), "T=%.1f,H=%.0f,P=%.0f",
           temperature, humidity, pressure);

  measurement->setValue(payload);
  if (deviceConnected) {
    measurement->notify();
  }

  Serial.println(payload);
}

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);
  sensorReady = bme.begin(BME280_ADDRESS, &Wire);

  BLEDevice::init(DEVICE_NAME);
  bleServer = BLEDevice::createServer();
  bleServer->setCallbacks(new ServerCallbacks());

  BLEService *service = bleServer->createService(SERVICE_UUID);
  measurement = service->createCharacteristic(
      MEASUREMENT_UUID,
      BLECharacteristic::PROPERTY_READ |
      BLECharacteristic::PROPERTY_NOTIFY);

  // CCCD lets the client enable or disable notifications.
  measurement->addDescriptor(new BLE2902());
  publishReading();
  service->start();

  BLEAdvertising *advertising = BLEDevice::getAdvertising();
  advertising->addServiceUUID(SERVICE_UUID);
  advertising->setScanResponse(true);
  advertising->setMinPreferred(0x06);
  advertising->setMaxPreferred(0x12);
  BLEDevice::startAdvertising();

  Serial.println("BLE ready. Scan for Nivotek-BME280.");
}

void loop() {
  const uint32_t now = millis();

  if (now - lastSampleAt >= SAMPLE_INTERVAL_MS) {
    lastSampleAt = now;
    publishReading();
  }

  if (!deviceConnected && wasConnected) {
    delay(500);  // Let the BLE stack finish disconnect handling.
    bleServer->startAdvertising();
    Serial.println("Disconnected; advertising restarted.");
    wasConnected = false;
  } else if (deviceConnected && !wasConnected) {
    Serial.println("BLE client connected.");
    wasConnected = true;
  }

  delay(5);
}

Callback chỉ đổi trạng thái kết nối. Việc đọc I2C, format chuỗi và gửi notification nằm trong loop(), nơi dễ kiểm soát thời gian hơn. Đây là thói quen tốt: đừng nhét delay(), thao tác cảm biến hoặc công việc nặng vào callback của BLE.

Read và Notify khác nhau thế nào?

Ứng dụng GATT trên điện thoại hiển thị custom service và characteristic của ESP32-S3 cạnh MacBook Pro.

GATT tổ chức dữ liệu theo service và characteristic; UUID giúp client tìm đúng thuộc tính cần đọc.

  • Read: điện thoại chủ động hỏi và nhận giá trị đang lưu trong characteristic.
  • Notify: điện thoại đăng ký một lần; ESP32 chủ động đẩy giá trị mới khi firmware gọi notify().

BLE2902 tạo Client Characteristic Configuration Descriptor (CCCD), nơi client bật hoặc tắt notification. Có thuộc tính PROPERTY_NOTIFY trong code chưa có nghĩa điện thoại tự nhận dữ liệu; bạn vẫn phải nhấn biểu tượng subscribe/notification trong ứng dụng.

Kiểm tra trên điện thoại

Điện thoại nhận nhiệt độ, độ ẩm và áp suất qua BLE Notify từ ESP32-S3 đang cấp nguồn đúng bằng USB-C từ MacBook Pro.

Sau khi bật Notify, giá trị mới được ESP32 chủ động gửi tới điện thoại mỗi hai giây.

  1. Nạp sketch và mở Serial Monitor ở 115200 baud.
  2. Mở ứng dụng GATT client, cấp quyền Bluetooth khi hệ điều hành hỏi và bắt đầu scan.
  3. Chọn Nivotek-BME280, sau đó kết nối.
  4. Tìm service UUID kết thúc bằng 1001, rồi characteristic kết thúc bằng 1002.
  5. Nhấn Read để xem giá trị hiện tại.
  6. Bật Notify/Subscribe; giá trị sẽ đổi khoảng hai giây một lần.
  7. Ngắt kết nối và scan lại. Serial phải in advertising restarted và thiết bị xuất hiện trở lại.

Nếu ứng dụng hiển thị hex, hãy đổi cách diễn giải sang UTF-8/ASCII. Ví dụ 54 3D 32 36 2E 34 chính là phần đầu của T=26.4.

Lỗi BLE thường gặp

Hiện tượngKiểm tra trước
Không thấy tên thiết bịĐúng board/port, log BLE ready, quyền Bluetooth và khoảng cách
Thấy thiết bị nhưng connect thất bạiNgắt client cũ, reset ESP32 và scan lại
Read được nhưng Notify không chạyBật subscribe và kiểm tra descriptor 0x2902
Giá trị hiện dạng hexChuyển data parser của ứng dụng sang UTF-8/ASCII
BME280 báo unavailableKiểm tra 0x76/0x77, SDA/SCL, 3V3 và GND
Mất thiết bị sau disconnectPhải gọi lại startAdvertising() sau khi ngắt
Firmware thiếu bộ nhớ sau khi thêm BLEBỏ code Wi-Fi không dùng; dự án BLE lớn nên cân nhắc NimBLE

ESP32-S3 dùng chung radio 2,4 GHz cho Wi-Fi và BLE. Hai giao thức có thể coexist, nhưng P6 cố ý không bật Wi-Fi để bạn debug từng đường truyền riêng trước.

Một lưu ý bảo mật quan trọng

Sketch này không yêu cầu pairing, encryption hay authentication. Thiết bị ở gần có thể thấy advertising và một client có thể kết nối để đọc dữ liệu. Điều đó chấp nhận được cho nhiệt độ trên bàn lab, nhưng không phù hợp với mật khẩu Wi-Fi, token, khóa cửa hoặc lệnh điều khiển nhạy cảm.

Khi dự án vượt khỏi demo, hãy định nghĩa rõ quyền Read/Write, pairing, bonding, mã hóa, reconnect và phiên bản payload. Bài giao tiếp BLE cho thiết bị IoT đi sâu hơn vào những phần đó cùng NimBLE và packet nhị phân.

Checklist hoàn thành P6

  • Điện thoại scan thấy Nivotek-BME280.
  • Bạn phân biệt được peripheral/central và server/client.
  • GATT client tìm thấy custom service và characteristic.
  • Read trả về dữ liệu BME280 hiện tại.
  • Notify cập nhật khoảng mỗi hai giây sau khi subscribe.
  • ESP32 advertising lại sau khi client disconnect.
  • Demo không chứa dữ liệu bí mật vì chưa bật bảo mật BLE.

P6 đã thêm một đường giao tiếp gần thiết bị, không phụ thuộc router. Hãy tiếp tục với phần cuối, P7: Làm dự án ESP32 hoàn chỉnh, để ghép GPIO, cảm biến và kết nối thành một thiết bị có thể kiểm thử.

Tham khảo

Chia sẻ

LinkedInFacebookX

Tiếp tục khám phá

Đọc tiếp

Bài viết liên quan

Xem thêm trong Bài viết

Nastrotek sử dụng cookie để phân tích truy cập và cá nhân hóa quảng cáo, giúp hiểu cách website được sử dụng. Bạn có thể chấp nhận hoặc từ chối các cookie không thiết yếu. Chính sách bảo mật