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ẻ

Ở 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

ESP32 quảng bá như peripheral; điện thoại đóng vai central để tìm và mở kết nối.
| Khái niệm | Vai trò trong bài |
|---|---|
| Peripheral | ESP32 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 server | Bả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 Libraryvà dependencyAdafruit 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?

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

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.
- Nạp sketch và mở Serial Monitor ở
115200baud. - Mở ứng dụng GATT client, cấp quyền Bluetooth khi hệ điều hành hỏi và bắt đầu scan.
- Chọn
Nivotek-BME280, sau đó kết nối. - Tìm service UUID kết thúc bằng
1001, rồi characteristic kết thúc bằng1002. - Nhấn Read để xem giá trị hiện tại.
- Bật Notify/Subscribe; giá trị sẽ đổi khoảng hai giây một lần.
- Ngắt kết nối và scan lại. Serial phải in
advertising restartedvà 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ượng | Kiể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ại | Ngắt client cũ, reset ESP32 và scan lại |
| Read được nhưng Notify không chạy | Bật subscribe và kiểm tra descriptor 0x2902 |
| Giá trị hiện dạng hex | Chuyển data parser của ứng dụng sang UTF-8/ASCII |
| BME280 báo unavailable | Kiểm tra 0x76/0x77, SDA/SCL, 3V3 và GND |
| Mất thiết bị sau disconnect | Phải gọi lại startAdvertising() sau khi ngắt |
| Firmware thiếu bộ nhớ sau khi thêm BLE | Bỏ 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ẻ
Tiếp tục khám phá
Đọc tiếp
Bài viết liên quan
ESP32 Beginner Guide – P5: Wi-Fi và Web Server
Kết nối ESP32 với Wi-Fi, lấy địa chỉ IP và tạo web server nội bộ để xem nhiệt độ, độ ẩm, áp suất BME280 trên trình duyệt.
ESP32 Beginner Guide – P7: Làm dự án ESP32 hoàn chỉnh
Khép lại series bằng Room Comfort Monitor: ESP32 đọc BME280, điều khiển LED bằng nút nhấn và hiển thị dashboard Wi-Fi nội bộ.
ESP32 Beginner Guide – P4: Kết nối cảm biến
Kết nối cảm biến với ESP32 qua digital, analog và I2C; thực hành đọc nhiệt độ, độ ẩm, áp suất từ BME280 và xử lý lỗi không tìm thấy thiết bị.