Ứng dụng Bluetooth Mesh đơn giản với ESP-IDF

Hello anh em đồng nhúng!

Sau 2 bài blog đầu tiên nói về nền tảng ESP-IDF và giao thức Bluetooth mesh, chắc hẳn anh em đã nắm được các khái niệm cốt lõi. Hôm nay, chúng ta sẽ bắt tay ngay vào lập trình để biến một bo mạch ESP32 đơn lẻ thành một Node có thể nhận lệnh BẬT/TẮT trong mạng lưới Mesh.

Mục tiêu : Bật tắt Led onboard điều khiển bằng smartphone thông qua giao thức Bluetooth mesh.




I, Chuẩn bị phần cứng :

Để bắt đầu lập trình và kiểm tra logic cơ bản của Node Server, chúng ta chỉ cần một bộ thiết bị tối thiểu. 1 board ESP32 sẵn có, và 1 led trên esp ( thường nối với gpio2) và 1 chiếc điện thoại thông minh có hỗ trợ Bluetooth để làm thiết bị provision và ra lệnh điều khiển.




II, Thiết lập môi trường

Theo như hướng dẫn từ blog trước, tạo 1 project mới để code :

Sau khi tạo xong, cần phải chỉnh sửa 1 số chỗ để có đủ thư viện và môi trường để lập trình ble mesh :

Để lập trình Bluetooth Mesh cho ESP32, bạn cần kích hoạt tính năng Bluetooth Mesh trong menuconfig. Lưu ý, theo mặc định, tùy chọn này thường bị ẩn và chưa được hiển thị trong menu chính.

Để bật được chức năng này, hãy tạo 1 file tên là sdkconfig.defaults nằm ở thư mục gốc của dự án, file này sẽ ghi đè các cấu hình mặc định của ESP-IDF, đảm bảo các tùy chọn cần thiết cho bluetooth mesh luôn bật.

Nội dung file như sau :


CONFIG_BT_ENABLED=y

# Bật chung tính năng Bluetooth (cần thiết cho cả BLE và Mesh).

CONFIG_BT_GATTS_SEND_SERVICE_CHANGE_MANUAL=y

# Kích hoạt chế độ gửi thông báo thay đổi dịch vụ (Service Change) GATTS thủ công.

# Thường được bật khi GATTS được sử dụng cùng với Mesh (ví dụ: cho Provisioning qua GATT).
CONFIG_BT_BTU_TASK_STACK_SIZE=4512

# Thiết lập kích thước Stack cho Task Bluetooth Utility (BTU).

# Mức 4512 bytes thường là đủ cho các ứng dụng BLE Mesh cơ bản.

CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y

# Lựa chọn bảng phân vùng (Partition Table) sử dụng một ứng dụng duy nhất

# với kích thước lớn (để chứa firmware lớn hơn).

# Thường cần thiết cho các dự án phức tạp hoặc khi kích hoạt nhiều tính năng Mesh.

CONFIG_BLE_MESH=y

# Kích hoạt toàn bộ ngăn xếp (Stack) Bluetooth Mesh. (Cấu hình cơ bản nhất).

CONFIG_BLE_MESH_NODE=y

# Cấu hình thiết bị này hoạt động như một Node trong mạng Mesh (chứ không phải Provisioner).

# Đây là vai trò phổ biến nhất cho các thiết bị ngoại vi.

CONFIG_BLE_MESH_PB_GATT=y

# Kích hoạt Provisioning Bearer qua GATT (PB-GATT).

# Cho phép Node được Provisioning thông qua kết nối BLE (thường dùng cho ứng dụng di động).

CONFIG_BLE_MESH_TX_SEG_MSG_COUNT=10

# Số lượng tin nhắn phân đoạn (Segmented Messages) tối đa mà Node có thể gửi

# cùng lúc (TX = Transmit).

# Tăng giá trị này có thể cải thiện thông lượng nhưng tốn bộ nhớ hơn.

CONFIG_BLE_MESH_RX_SEG_MSG_COUNT=10

# (Giả định giá trị 10) Số lượng tin nhắn phân đoạn tối đa mà Node có thể nhận

# cùng lúc (RX = Receive).
# Giá trị phải đủ lớn để xử lý các tin nhắn lớn nhận được từ mạng.

Sau khi lưu file, dùng esp terminal để vào menuconfig, để bật tính năng Mesh của BLE bằng lệnh :





Cửa sổ cấu hình hiện ra, vào Component config bạn sẽ thấy ESP-BLE-MESH support, nhấn space để enable nó, và enter để vào các cấu hình sâu hơn.



Tiếp theo, vì Bluetooth Mesh được xây dựng trên nền tảng Bluetooth Low Energy (BLE), nên việc khởi tạo ngăn xếp BLE là bước bắt buộc đầu tiên trước khi chúng ta khởi động Mesh. Nếu không có BLE, Mesh sẽ không có giao thức truyền tải để hoạt động.

Vì blog này tập chung tối đa cho mesh, nên ta sẽ sử dụng component example_init có sẵn trong ESP-IDF, nó giúp đóng gói các hàm khởi tạo phức tạp vào 1 hàm bluetooth_init() duy nhất, cho phép chúng ta đơn giản hóa phần khởi tạo BLE.

Để include được component này vào project của chúng ta, cần thực hiện như sau :

Tạo thêm 1 file idf_component.yml nằm trong thư mục main, sau đó viết vào file nội dung như sau :

	dependencies:
	example_init:
	path: ${IDF_PATH}/examples/bluetooth/esp_ble_mesh/common_components/example_init

Sau khi hoàn thành tất cả các bước trên, môi trường đã sẵn sàng để chúng ta bắt tay vào code bluetooth mesh.


III, Coding

3.1, Mã nguồn :



3.2, Include các thư viện cần thiết :

Bluetooth Mesh là một giao thức mạnh mẽ nhưng phức tạp, vì vậy để kiểm soát và tương tác được với ngăn xếp (stack) này, chúng ta cần phải gọi đúng các hàm API được định nghĩa trong nhiều header files khác nhau, bao gồm các thư viện như sau :

/* -------------------------------------------------------------------------- */
/* 1. C Standard & System Libraries          */
/* -------------------------------------------------------------------------- */
#include <stdio.h>   // Thư viện nhập/xuất chuẩn (printf) để debug
#include <string.h>   // Xử lý chuỗi và bộ nhớ (memcpy, memset,...)
#include <inttypes.h>  // Định nghĩa kiểu số nguyên chuẩn (uint8_t, uint32_t,...)

#include "esp_log.h"  // Ghi log hệ thống (ESP_LOGI, ESP_LOGE)
#include "nvs_flash.h" // Lưu trữ NVS (NetKey, AppKey, Sequence Number)
#include "esp_bt.h"   // Quản lý Bluetooth Controller & Host Stack

/* -------------------------------------------------------------------------- */
/* 2. ESP-IDF Bluetooth Mesh APIs           */
/* -------------------------------------------------------------------------- */
#include "esp_ble_mesh_defs.h"       // Định nghĩa Core: Structs, Opcodes, Model IDs, Error Codes
#include "esp_ble_mesh_common_api.h"    // API chung: Init/Deinit stack, lấy thông tin cơ bản
#include "esp_ble_mesh_networking_api.h"  // Networking: Gửi/nhận tin nhắn (Send/Recv), quản lý Group Addr
#include "esp_ble_mesh_provisioning_api.h" // Provisioning: Xử lý quá trình gia nhập mạng (Node/Provisioner)
#include "esp_ble_mesh_config_model_api.h" // Config Models: Binding AppKey, Subscription (Client/Server)
#include "esp_ble_mesh_local_data_operation_api.h" // Local Ops: Thao tác dữ liệu nội bộ (Composition Data)

/* -------------------------------------------------------------------------- */
/* 3. Custom/Example Helpers              */
/* -------------------------------------------------------------------------- */
#include "ble_mesh_example_init.h"     // Helper: Hỗ trợ khởi tạo nhanh NVS, BT Controller & Stack


VI, Luồng hoạt động của chương trình :

Luồng hoạt động chương trình được chia thành ba giai đoạn chính, quản lí và khởi tạo đầu tiên thông qua hàm chính và các hàm Callback ( xử lí sự kiện).

4.1, Khởi tạo và cấu hình :

Bắt đầu với hàm main, các hàm sau được gọi để ESP32 khởi động và cấu hình để khởi tạo BLE mesh :

 esp_err_t err;
	 ESP_LOGI(TAG, "Initializing...");
	
	 err = nvs_flash_init();
	 if (err == ESP_ERR_NVS_NO_FREE_PAGES) {
	 ESP_ERROR_CHECK(nvs_flash_erase());
	 err = nvs_flash_init();
	 }
	 ESP_ERROR_CHECK(err);

Hàm này có nhiệm vụ khởi tạo vùng nhớ NVS nằm trên bộ nhớ Flash để lưu key, địa chỉ, cấu hình của Mesh, thường được gọi đầu tiên khi chương trình chạy,

( hàm trong ESP-IDF thường có kiểu trả về là esp_err_t sử dụng để check lỗi), nếu trả lỗi là ESP_ERR_NVS_NO_FREE_PAGES (không còn chỗ trống trên nvs) sẽ gọi hàm xóa nvs_flash_erase() để có chỗ ghi thông tin cho mesh.

Tiếp theo là hàm khởi tạo cho BLE stack :

 err = bluetooth_init();
 if (err) {
 ESP_LOGE(TAG, "esp32_bluetooth_init failed (err %d)", err);
 return;
 }

Nhiệm vụ: Hàm bluetooth_init() (được cung cấp bởi component example_init) thực hiện tất cả các bước cần thiết để khởi động nền tảng Bluetooth Low Energy (BLE) trên ESP32, bao gồm các nhiệm vụ sau :

  • Khởi tạo NVS: Khởi tạo bộ nhớ không bay hơi (Non-Volatile Storage) để Node lưu trữ các thông số cấu hình và khóa Mesh.
  • Khởi tạo Controller: Khởi động phần cứng Bluetooth trên chip ESP32.
  • Khởi tạo Host Stack: Khởi động ngăn xếp phần mềm Bluetooth Host, nơi các logic cấp cao hơn như Mesh sẽ chạy.

Hàm trả về 1 biến kiểu esp_err_t để check lỗi, nếu có lỗi xảy ra, chương trình nhảy vào block if và log ra màn hình để debug.

Sau khi khởi tạo stack Bluetooth xong, ta sẽ đến bước khởi tạo cho Bluetooth mesh

err = ble_mesh_init();
 if (err) {
 ESP_LOGE(TAG, "Bluetooth mesh init failed (err %d)", err);
 }

Hàm ble_mesh_init(void) là hàm cốt lõi, đảm nhiệm việc khởi tạo và kích hoạt toàn bộ Stack Bluetooth mesh trên thiết bị ESP32 để nó có thể hoạt động như một Node trong mạng Mesh.

Nó tập hợp các cấu hình, đăng ký các hàm xử lý sự kiện (callbacks), và bật tính năng Provisioning.

Hàm được thiết kế như sau :

	esp_err_t ble_mesh_init(void)
	{
	 esp_err_t err = ESP_OK;
	 esp_ble_mesh_register_prov_callback(example_ble_mesh_provisioning_cb);
	 esp_ble_mesh_register_config_server_callback(example_ble_mesh_config_server_cb);
	 esp_ble_mesh_register_generic_server_callback(example_ble_mesh_generic_server_cb);
	 err = esp_ble_mesh_init(&provision, &composition);
	 if (err != ESP_OK) {
	    ESP_LOGE(TAG, "Failed to initialize mesh stack (err %d)", err);
	    return err;
	 }
	 err = esp_ble_mesh_node_prov enable((esp_ble_mesh_prov_bearer_t)(ESP_BLE_MESH_PROV_ADV | ESP_BLE_MESH_PROV_GATT));
	 if (err != ESP_OK) {
  	    ESP_LOGE(TAG, "Failed to enable mesh node (err %d)", err);
    	 return err;
     }
     ESP_LOGI(TAG, "BLE Mesh Node initialized");
     return err;
    }
      

Hàm này đảm nhiệm việc đăng ký 3 hàm Callback( hàm xử lí sự kiện) với Stack ESP-BLE-MESH để khi có sự kiện tương ứng sẽ gọi đến các hàm này.

Tiếp đến là hàm khởi tạo Stack BLE mesh : esp_ble_mesh_init(), với 2 tham số truyền vào là 2 con trỏ của 2 struct provision, composition được định nghĩa ở trên :

static esp_ble_mesh_comp_t composition = {

.cid = CID_ESP,

.element_count = ARRAY_SIZE(elements),

.elements = elements,

};

static esp_ble_mesh_prov_t provision = {

.uuid = dev_uuid,

.output_size = 0,

.output_actions = 0,

};

Với struct esp_ble_mesh_comp_t là hồ sơ kỹ thuật của Node, mô tả cho provisioner( Thiết bị thêm node vào mạng) biết node này có những tính năng, element và model nào, cụ thể là các trường :

  • cid = CID_ESP: Đây là Company Identifier (CID), dùng để nhận diện nhà sản xuất. CID_ESP là hằng số cho Espressif Systems (0x02E5).
  • element_count = ARRAY_SIZE(elements): Số lượng Element (địa chỉ logic) mà Node này hỗ trợ. ARRAY_SIZE(elements) là một macro tính toán kích thước của mảng elements.
  • elements = elements: Con trỏ tới mảng elements, nơi bạn định nghĩa chi tiết các Model.

Với struct provision định nghĩa các khả năng của Node trong quá trình Provisioning :

  • uuid = dev_uuid: Device UUID (Universally Unique Identifier). Đây là ID duy nhất mà Node phát ra trước khi được Provisioning, cho phép Provisioner nhận diện nó.
  • output_size = 0
  • output_actions = 0

Khi để 2 trường dưới bằng 0 thì Node đang được cấu hình ở chế độ No OOB (Out-of-Band) Authentication, nhằm để đơn giản hóa Provisioning làm cho quá trình thêm Node vào mạng diễn ra ngay lập tức mà không cần bất kỳ bước xác minh nào.

Sau khi định nghĩa các data cần thiết cho quá trình provision và đăng ký Callback, tiếp theo sẽ là lệnh cho ESP32 bắt đầu phát tín hiệu, mở các kết nối để Provisioner có thể bắt đầu quá trình provision bằng API :

esp_ble_mesh_node_prov_enable()

Với tham số truyền vào hàm là 1 biến kiểu (esp_ble_mesh_prov_bearer_t). Tham số này định nghĩa các Bearer (Phương thức truyền tải) mà Node sẽ sử dụng để Provisioning :

ESP_BLE_MESH_PROV_ADV : Cho phép Provisioning qua quảng cáo BLE Advertising (gọi là PB-ADV). Phương thức này phù hợp khi Provisioner ở gần và không cần kết nối GATT cố định.

ESP_BLE_MESH_PROV_GATT : Cho phép Provisioning qua kết nối BLE GATT (gọi là PB-GATT). Phương thức này thường được sử dụng khi Provisioner là ứng dụng di động.

Code thực hiện phép | để kích hoạt cả 2 phương pháp, đảm bảo tương thức với Provisioner khác nhau.

4.2, Định nghĩa cấu trúc Node.

Phần ở đầu source code chứa các định nghĩa cho các thành phần logic của 1 node Bluetooth mesh :

/* Cấu hình Configuration Server Model */
static esp_ble_mesh_cfg_srv_t config_server = {
    /* 1. RELAY: Tính năng chuyển tiếp tin nhắn */
    // Ở đây ta để DISABLED nghĩa là Node này chỉ nhận lệnh, không giúp chuyển tin đi tiếp.
    // Phù hợp cho các thiết bị chạy pin (Low Power).
    .relay = ESP_BLE_MESH_RELAY_DISABLED,

    /* 2. BEACON: Phát tín hiệu nhận diện */
    // ENABLED để Node phát ra tín hiệu "Tôi vẫn ổn định" giúp bảo mật mạng lưới.
    .beacon = ESP_BLE_MESH_BEACON_ENABLED,

    /* 3. NETWORK TRANSMIT: Cơ chế "Nói lại cho chắc" */
    // ESP_BLE_MESH_TRANSMIT(count, interval_ms)
    // (2, 20) nghĩa là: Mỗi tin nhắn sẽ được gửi gốc 1 lần + gửi lại (re-transmit) 2 lần nữa.
    // Tổng cộng 3 lần gửi, mỗi lần cách nhau 20ms. Giúp tin nhắn không bị lạc trôi.
    .net_transmit = ESP_BLE_MESH_TRANSMIT(2, 20),

    /* 4. DEFAULT TTL (Time To Live): Tuổi thọ gói tin */
    // Số 7 nghĩa là gói tin được phép nhảy qua tối đa 7 Node trung gian.
    // Nếu đi qua 7 bước mà chưa tới đích, tin nhắn sẽ tự hủy để tránh rác mạng.
    .default_ttl = 7,

    /* 5. TÍNH NĂNG NÂNG CAO (GATT Proxy & Friend) */
    // Thường được bật/tắt linh động qua Menuconfig (sdkconfig)
#if CONFIG_BLE_MESH_GATT_PROXY_SERVER
    .gatt_proxy = ESP_BLE_MESH_GATT_PROXY_ENABLED, // Cho phép điện thoại kết nối vào Mesh qua Node này
#else
    .gatt_proxy = ESP_BLE_MESH_GATT_PROXY_NOT_SUPPORTED,
#endif
    .friend_state = ESP_BLE_MESH_FRIEND_DISABLED,
};


1. dev_uuid: Device UUID (16 byte). Đây là ID duy nhất mà Node phát ra trước khi được Provisioning, cho phép Provisioner nhận dạng. Giá trị khởi tạo 0xdd, 0xdd chỉ là placeholder (giữ chỗ).

2. esp_ble_mesh_cfg_srv_t config_server : Struct này mô tả cách Node hoạt động trong mạng lưới.

Sau khi đã có cấu hình cơ bản, chúng ta cần định nghĩa các Model (chức năng) và Element (thành phần chứa chức năng). Đây là bước quan trọng để Mesh Stack hiểu thiết bị này làm được những gì.

/* -------------------------------------------------------------------------- */
/* 1. Định nghĩa Publication Context (Khả năng gửi tin)                       */
/* -------------------------------------------------------------------------- */
// ESP_BLE_MESH_MODEL_PUB_DEFINE(name, size, role);
// - name: Tên biến quản lý publication (onoff_pub_0).
// - size (2+3): Kích thước bộ đệm tin nhắn đầu ra. 
//   (2 bytes cho Opcode + 3 bytes cho dữ liệu trạng thái OnOff).
// - role: Vai trò của Node (ở đây là Node thường, không phải Provisioner).
ESP_BLE_MESH_MODEL_PUB_DEFINE(onoff_pub_0, 2 + 3, ROLE_NODE);

/* -------------------------------------------------------------------------- */
/* 2. Cấu hình Generic OnOff Server (Logic xử lý bật/tắt)                     */
/* -------------------------------------------------------------------------- */
static esp_ble_mesh_gen_onoff_srv_t onoff_server_0 = {
    // Cấu hình phản hồi tự động (Response Control)
    .rsp_ctrl = {
        // Khi nhận lệnh GET (Hỏi trạng thái), stack tự động trả lời, không cần code tay.
        .get_auto_rsp = ESP_BLE_MESH_SERVER_AUTO_RSP, 
        // Khi nhận lệnh SET (Ra lệnh bật/tắt), stack tự động gửi lại trạng thái mới nhất.
        .set_auto_rsp = ESP_BLE_MESH_SERVER_AUTO_RSP, 
    },
};

/* -------------------------------------------------------------------------- */
/* 3. Khai báo danh sách các Model trong Element                              */
/* -------------------------------------------------------------------------- */
static esp_ble_mesh_model_t root_models[] = {
    // Model 1: Configuration Server (Bắt buộc phải có)
    // Dùng để quản lý cấu hình mạng (AppKey, NetKey, Relay...).
    // Biến &config_server đã định nghĩa ở phần trước.
    ESP_BLE_MESH_MODEL_CFG_SRV(&config_server),

    // Model 2: Generic OnOff Server (Ứng dụng chính)
    // &onoff_pub_0: Dùng để gửi trạng thái (Publish) khi đèn thay đổi.
    // &onoff_server_0: Chứa cấu hình phản hồi tự động.
    ESP_BLE_MESH_MODEL_GEN_ONOFF_SRV(&onoff_pub_0, &onoff_server_0),
};

/* -------------------------------------------------------------------------- */
/* 4. Đóng gói vào Element (Thành phần vật lý ảo)                             */
/* -------------------------------------------------------------------------- */
static esp_ble_mesh_elem_t elements[] = {
    // ESP_BLE_MESH_ELEMENT(Loc, Root_Models, Vendor_Models);
    // - Loc (0): Location ID (vị trí vật lý, ví dụ 0 là main board).
    // - root_models: Danh sách các Model chuẩn SIG (như OnOff, Lightness...).
    // - ESP_BLE_MESH_MODEL_NONE: Không dùng Model của hãng thứ 3 (Vendor Model).
    ESP_BLE_MESH_ELEMENT(0, root_models, ESP_BLE_MESH_MODEL_NONE),
};