Bluetooth Low Energy (BLE) là gì: quảng bá, GATT, dịch vụ, đặc tính và ESP32
- 1. BLE khác Bluetooth cổ điển thế nào?
- 2. Hai giai đoạn: quảng bá và kết nối
- 3. GATT: dịch vụ, đặc tính, mô tả
- 4. Bốn thao tác trên một đặc tính
- 5. Chuẩn bị
- 6. Chương trình 1: ESP32 làm thiết bị BLE
- 7. Thử bằng điện thoại
- 8. Chương trình 2: quét các gói quảng bá
- 9. Kết quả mong đợi
- 10. Lưu ý khi dùng thật
- 11. Xử lý lỗi thường gặp
- 12. Xem thêm
Giải thích Bluetooth Low Energy: quảng bá và kết nối, cấu trúc GATT gồm dịch vụ, đặc tính, mô tả, UUID. Hướng dẫn cho ESP32 làm thiết bị BLE có đặc tính đọc, ghi, thông báo và thử bằng điện thoại.
Bluetooth Low Energy (BLE) là nhánh tiết kiệm điện của Bluetooth, có mặt trong vòng đeo tay, cảm biến chạy pin, khóa thông minh và hầu hết điện thoại hiện nay. BLE không truyền một luồng byte như cổng nối tiếp; nó tổ chức dữ liệu thành các giá trị có tên để bên kia đọc, ghi hoặc đăng ký nhận thông báo. Bài viết giải thích hai giai đoạn quảng bá và kết nối, cấu trúc GATT gồm dịch vụ, đặc tính, mô tả, rồi cho ESP32 làm một thiết bị BLE hoàn chỉnh để thử bằng điện thoại.
1. BLE khác Bluetooth cổ điển thế nào?
| Tiêu chí | Bluetooth cổ điển | BLE |
|---|---|---|
| Mục đích | Truyền liên tục: âm thanh, cổng nối tiếp không dây | Truyền ít dữ liệu, ngắt quãng |
| Cách trao đổi | Luồng byte giống UART | Đọc, ghi các giá trị có cấu trúc (GATT) |
| Điện năng | Cao hơn, kết nối luôn hoạt động | Rất thấp, hai bên ngủ giữa các nhịp trao đổi |
| Điện thoại | Android; iPhone không mở kiểu cổng nối tiếp cho thiết bị tự làm | Android và iPhone |
| Thư viện trên ESP32 | BluetoothSerial | BLE (BLEDevice, BLEServer…) |
Hai loại cùng dùng băng tần 2,4 GHz nhưng không nói chuyện được với nhau. ESP32 hỗ trợ cả hai. Bluetooth cổ điển kiểu cổng nối tiếp đã có trong bài điều khiển relay bằng điện thoại qua Bluetooth.
2. Hai giai đoạn: quảng bá và kết nối
- Quảng bá (advertising): thiết bị ngoại vi phát đều đặn những gói rất ngắn chứa tên và UUID của dịch vụ chính. Chưa có kết nối nào; mọi thiết bị đang quét ở gần đều nhận được.
- Kết nối (connection): thiết bị trung tâm, thường là điện thoại, chọn một thiết bị đang quảng bá và gửi yêu cầu kết nối. Từ đó hai bên trao đổi riêng với nhau theo nhịp đã hẹn và ngủ trong thời gian còn lại.
- Thiết bị ngoại vi (peripheral) là bên quảng bá và chờ; thiết bị trung tâm (central) là bên quét và chủ động kết nối.
- Sau khi kết nối, bên giữ dữ liệu gọi là máy chủ GATT (server), bên đọc ghi gọi là máy khách GATT (client). Thông thường thiết bị ngoại vi là máy chủ.
- Một số thiết bị chỉ quảng bá mà không bao giờ kết nối, ví dụ thẻ định vị và cảm biến phát số đo ngay trong gói quảng bá.
3. GATT: dịch vụ, đặc tính, mô tả
- Dịch vụ (service) gom các giá trị liên quan thành một nhóm, ví dụ dịch vụ pin, dịch vụ nhịp tim.
- Đặc tính (characteristic) là một giá trị cụ thể trong dịch vụ, kèm thuộc tính cho biết được phép làm gì với nó: đọc, ghi, thông báo.
- Mô tả (descriptor) là thông tin phụ gắn với đặc tính. Quan trọng nhất là mô tả 0x2902: máy khách ghi vào đây để bật hoặc tắt việc nhận thông báo.
- UUID là mã nhận diện của từng dịch vụ, đặc tính, mô tả. UUID 16 bit dành cho các loại chuẩn; dịch vụ tự định nghĩa dùng UUID 128 bit tạo ngẫu nhiên.
- Giá trị của đặc tính chỉ là một dãy byte. BLE không quy định ý nghĩa; chương trình ở hai phía phải thống nhất với nhau. Với thiết lập mặc định, mỗi thông báo mang tối đa 20 byte.
4. Bốn thao tác trên một đặc tính
Thông báo là điểm mạnh của BLE: thay vì điện thoại hỏi liên tục, ESP32 chỉ gửi khi có số liệu mới, nhờ vậy cả hai bên đều tiết kiệm pin.
5. Chuẩn bị
- Một mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340); thêm một mạch nữa nếu muốn chạy hai chương trình cùng lúc.
- Cáp USB để nạp chương trình. Không cần linh kiện ngoài: bài dùng đèn LED có sẵn trên mạch (GPIO2).
- Một điện thoại có Bluetooth và một ứng dụng quét BLE, loại cho xem dịch vụ, đặc tính và cho đọc, ghi, nhận thông báo.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Thư viện BLE có sẵn trong gói, không cần cài thêm.
6. Chương trình 1: ESP32 làm thiết bị BLE
/*
PYWORLD.VN - BLE với ESP32: thiết bị ngoại vi có một dịch vụ và hai đặc tính, thử bằng ứng dụng quét BLE trên điện thoại
Đặc tính ĐÈN (đọc, ghi): 1 byte, ghi 01 để bật và 00 để tắt LED GPIO2
Đặc tính THỜI GIAN (đọc, thông báo): số giây đã chạy, 4 byte, byte thấp trước; gửi thông báo mỗi giây
*/
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#define LED_PIN 2
const char *TEN_THIET_BI = "PYWORLD-BLE";
// UUID 128 bit tự đặt cho dịch vụ và hai đặc tính của bài
#define UUID_DICH_VU "c0de0001-7a4b-4e2f-9b1d-5f3a8e6c2d10"
#define UUID_DEN "c0de0002-7a4b-4e2f-9b1d-5f3a8e6c2d10"
#define UUID_THOI_GIAN "c0de0003-7a4b-4e2f-9b1d-5f3a8e6c2d10"
BLECharacteristic *dacTinhThoiGian = nullptr;
volatile bool dangKetNoi = false;
// Thư viện gọi các hàm này khi có thiết bị kết nối hoặc ngắt kết nối
class SuKienKetNoi : public BLEServerCallbacks {
void onConnect(BLEServer *mayChu) override {
dangKetNoi = true;
}
void onDisconnect(BLEServer *mayChu) override {
dangKetNoi = false;
}
};
// Thư viện gọi hàm này khi điện thoại ghi vào đặc tính ĐÈN
class SuKienGhiDen : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *dacTinh) override {
if (dacTinh->getLength() < 1) {
return;
}
uint8_t giaTri = dacTinh->getData()[0];
bool bat = (giaTri != 0x00 && giaTri != '0'); // Nhận cả byte 01 lẫn ký tự '1' gõ dưới dạng chữ
digitalWrite(LED_PIN, bat ? HIGH : LOW);
Serial.printf("Nhan lenh ghi: 0x%02X -> %s LED\n", giaTri, bat ? "BAT" : "TAT");
}
};
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
BLEDevice::init(TEN_THIET_BI);
BLEServer *mayChu = BLEDevice::createServer();
mayChu->setCallbacks(new SuKienKetNoi());
BLEService *dichVu = mayChu->createService(UUID_DICH_VU);
BLECharacteristic *dacTinhDen = dichVu->createCharacteristic(
UUID_DEN, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE);
dacTinhDen->setCallbacks(new SuKienGhiDen());
uint8_t dangTat = 0x00;
dacTinhDen->setValue(&dangTat, 1);
dacTinhThoiGian = dichVu->createCharacteristic(
UUID_THOI_GIAN, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
dacTinhThoiGian->addDescriptor(new BLE2902()); // Mô tả 0x2902: nơi điện thoại bật, tắt việc nhận thông báo
dacTinhThoiGian->setValue((uint32_t)0);
dichVu->start();
// Gói quảng bá mang UUID của dịch vụ để bên quét nhận ra thiết bị
BLEAdvertising *quangBa = BLEDevice::getAdvertising();
quangBa->addServiceUUID(UUID_DICH_VU);
quangBa->setScanResponse(true);
BLEDevice::startAdvertising();
Serial.printf("\nThiet bi BLE \"%s\", dia chi %s, dang quang ba\n",
TEN_THIET_BI, BLEDevice::getAddress().toString().c_str());
}
void loop() {
static unsigned long lucCapNhatCuoi = 0;
static bool truocDoKetNoi = false;
// Mỗi giây cập nhật giá trị; chỉ gửi thông báo khi đang có kết nối
if (millis() - lucCapNhatCuoi >= 1000) {
lucCapNhatCuoi = millis();
uint32_t soGiay = millis() / 1000;
dacTinhThoiGian->setValue(soGiay); // 4 byte, byte thấp trước
if (dangKetNoi) {
dacTinhThoiGian->notify();
}
}
if (dangKetNoi && !truocDoKetNoi) {
Serial.println("Co thiet bi ket noi");
}
if (!dangKetNoi && truocDoKetNoi) {
delay(500); // Chờ ngăn xếp Bluetooth dọn xong kết nối cũ
BLEDevice::startAdvertising();
Serial.println("Thiet bi da ngat ket noi, quang ba lai");
}
truocDoKetNoi = dangKetNoi;
delay(10);
}
BLEDevice::init()khởi động Bluetooth và đặt tên sẽ xuất hiện khi quét.- Trình tự dựng cây GATT: tạo máy chủ, tạo dịch vụ từ UUID, tạo từng đặc tính với UUID và thuộc tính của nó, rồi
start()dịch vụ. - Thư viện báo sự kiện qua các lớp callback:
onConnect(),onDisconnect()khi kết nối thay đổi,onWrite()khi điện thoại ghi vào đặc tính. Các hàm này chạy trong tác vụ của Bluetooth, không phải trongloop(), nên cần viết ngắn gọn. - Đặc tính có thuộc tính thông báo phải có mô tả 0x2902, thêm bằng
addDescriptor(new BLE2902()). setValue(soGiay)với số kiểuuint32_tlưu 4 byte theo thứ tự byte thấp trước;notify()gửi giá trị đó cho máy khách đã bật thông báo.- Khi máy khách ngắt kết nối, thiết bị phải quảng bá lại thì lần sau mới tìm thấy.
- Ba UUID trong bài là số tự đặt. Sản phẩm riêng nên tạo bộ UUID ngẫu nhiên mới để không trùng với thiết bị khác.
Thư viện BLE chiếm nhiều bộ nhớ chương trình: một chương trình BLE đơn giản như trên đã dùng hơn ba phần tư vùng chứa mặc định của mạch. Nếu ghép thêm Wi-Fi mà báo chương trình quá lớn, chọn Tools > Partition Scheme loại có vùng ứng dụng lớn hơn.
7. Thử bằng điện thoại
- Nạp chương trình 1, mở Tools > Serial Monitor ở 115200 baud để xem tên và địa chỉ của thiết bị.
- Mở ứng dụng quét BLE trên điện thoại, tìm PYWORLD-BLE và bấm kết nối. Không vào phần cài đặt Bluetooth của điện thoại để ghép đôi: thiết bị BLE kiểu này làm việc qua ứng dụng.
- Mở dịch vụ có UUID bắt đầu bằng
c0de0001. Ở đặc tínhc0de0002, ghi byte01: đèn LED sáng. Ghi00: đèn tắt. - Ở đặc tính
c0de0003, bấm đọc để lấy số giây một lần, hoặc bật nhận thông báo để giá trị tự cập nhật mỗi giây. - Giá trị hiện dưới dạng bốn byte, ví dụ
7D 00 00 00. Byte thấp đi trước nên số thật là 0x0000007D, tức 125 giây.
8. Chương trình 2: quét các gói quảng bá
Chương trình này cho ESP32 đóng vai bên quét, giống bước đầu tiên của điện thoại. Nó chỉ nghe gói quảng bá, không kết nối với thiết bị nào.
/*
PYWORLD.VN - BLE với ESP32: quét các gói quảng bá xung quanh, in địa chỉ, cường độ tín hiệu và tên từng thiết bị
Mỗi lượt quét dài 5 giây; thiết bị nào quảng bá dịch vụ của chương trình 1 được đánh dấu riêng
Không cần linh kiện ngoài; có hai mạch ESP32 thì nạp chương trình 1 cho mạch kia để thấy nó trong danh sách
*/
#include <BLEDevice.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#define UUID_DICH_VU "c0de0001-7a4b-4e2f-9b1d-5f3a8e6c2d10"
const int THOI_GIAN_QUET_GIAY = 5;
BLEScan *boQuet = nullptr;
void setup() {
Serial.begin(115200);
BLEDevice::init("");
boQuet = BLEDevice::getScan();
boQuet->setActiveScan(true); // Quét chủ động: hỏi thêm gói trả lời quét, thường chứa tên thiết bị
boQuet->setInterval(100);
boQuet->setWindow(99);
Serial.println("\nQuet thiet bi BLE xung quanh");
}
void loop() {
BLEScanResults *ketQua = boQuet->start(THOI_GIAN_QUET_GIAY, false);
int soThietBi = ketQua->getCount();
Serial.printf("--- Tim thay %d thiet bi ---\n", soThietBi);
for (int i = 0; i < soThietBi; i++) {
BLEAdvertisedDevice thietBi = ketQua->getDevice(i);
String ten = thietBi.haveName() ? thietBi.getName() : String("(khong ten)");
bool coDichVuCuaBai = thietBi.isAdvertisingService(BLEUUID(UUID_DICH_VU));
Serial.printf("%2d. %s | %4d dBm | %s%s\n", i + 1, thietBi.getAddress().toString().c_str(),
thietBi.getRSSI(), ten.c_str(), coDichVuCuaBai ? " | co dich vu cua bai" : "");
}
boQuet->clearResults(); // Giải phóng bộ nhớ của lượt quét vừa xong
delay(3000);
}
- Quét chủ động (
setActiveScan(true)): ngoài gói quảng bá, ESP32 hỏi thêm gói trả lời quét, nơi nhiều thiết bị đặt tên của mình. - Cường độ tín hiệu tính bằng dBm, là số âm: càng gần 0 càng mạnh. Khoảng −50 dBm là rất gần, dưới −90 dBm là sắp mất tín hiệu.
isAdvertisingService()kiểm tra gói quảng bá có chứa UUID dịch vụ của bài hay không. Đây là cách một ứng dụng tìm đúng thiết bị của mình giữa nhiều thiết bị khác.- Danh sách có cả thiết bị của những người xung quanh. Điện thoại và tai nghe thường dùng địa chỉ ngẫu nhiên thay đổi theo thời gian, nên địa chỉ của chúng không cố định giữa các lần quét.
9. Kết quả mong đợi
- Chương trình 1 in một dòng khi có thiết bị kết nối, một dòng cho mỗi lệnh ghi và một dòng khi ngắt kết nối.
- Chương trình 2 in danh sách sau mỗi lượt quét 5 giây. Nếu mạch chạy chương trình 1 ở gần và chưa có ai kết nối, nó xuất hiện kèm ghi chú có dịch vụ của bài.
- Khi điện thoại đang kết nối với mạch chạy chương trình 1, mạch đó ngừng quảng bá và biến mất khỏi danh sách quét.
10. Lưu ý khi dùng thật
- Chưa có bảo mật: chương trình 1 không yêu cầu ghép đôi hay mã hóa, nên bất kỳ ai ở gần có ứng dụng quét BLE cũng kết nối và ghi được. Thiết bị điều khiển thứ quan trọng cần bật ghép đôi có mã và mã hóa kết nối; thư viện BLE có sẵn lớp
BLESecuritycho việc này. - Mỗi lúc một kết nối: với cách viết trong bài, thiết bị ngừng quảng bá khi đã có một máy khách kết nối.
- Dữ liệu dài: giá trị lớn hơn 20 byte phải chia thành nhiều thông báo, hoặc hai bên thỏa thuận kích thước gói lớn hơn.
- Dùng chung với Wi-Fi: BLE và Wi-Fi chung một bộ thu phát 2,4 GHz trên ESP32. Chạy cả hai được nhưng tốn bộ nhớ và mỗi bên chậm đi.
- Tiết kiệm pin: lợi thế về điện năng của BLE chỉ thấy rõ khi kết hợp với các chế độ ngủ của ESP32; để nguyên như trong bài, mạch vẫn tiêu thụ như khi bật radio liên tục.
11. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Ứng dụng không tìm thấy PYWORLD-BLE | Chưa bật Bluetooth hoặc chưa cấp quyền cho ứng dụng; thiết bị đang kết nối với máy khác | Bật Bluetooth, cấp các quyền ứng dụng yêu cầu; ngắt kết nối ở máy kia |
| Tìm trong cài đặt Bluetooth của điện thoại không ghép đôi được | Thiết bị BLE trong bài không dùng ghép đôi của hệ thống | Kết nối từ ứng dụng quét BLE |
| Ghi 1 mà đèn không sáng | Ứng dụng gửi dữ liệu ở dạng khác với mong đợi | Xem Serial Monitor in byte nào; chọn kiểu byte (hex) và nhập 01 |
| Không thấy giá trị thời gian tự đổi | Chưa bật nhận thông báo cho đặc tính | Bấm nút nhận thông báo của đặc tính c0de0003 |
| Điện thoại vẫn hiện tên hoặc dịch vụ cũ sau khi sửa chương trình | Điện thoại lưu tạm thông tin của lần kết nối trước | Tắt rồi bật lại Bluetooth của điện thoại, quét lại |
| Báo chương trình quá lớn khi thêm Wi-Fi | BLE và Wi-Fi cùng lúc vượt vùng chứa mặc định | Chọn Partition Scheme có vùng ứng dụng lớn hơn |
| Chương trình 2 không thấy mạch chạy chương trình 1 | Mạch đó đang có điện thoại kết nối nên không quảng bá | Ngắt kết nối trên điện thoại rồi chờ lượt quét sau |
12. Xem thêm
- Điều khiển relay bằng điện thoại qua Bluetooth cổ điển: cách truyền kiểu cổng nối tiếp để so sánh với BLE.
- Wi-Fi trên ESP32: quét mạng và kết nối.
- Deep sleep trên ESP32: nền tảng để thiết bị BLE chạy pin lâu.
Xem thêm