Ứ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),
};


4.3, Các hàm xử lý callback

Sau khi khởi tạo xong, chương trình sẽ không còn chạy tuần tự từ trên xuống dưới nữa mà chuyển sang cơ chế Event-driven (Hướng sự kiện). Lúc này, chương trình sẽ ở trạng thái lắng nghe sự kiện. Các hàm xử lý sẽ chỉ được tự động kích hoạt khi có sự kiện gửi về từ BLE Mesh Stack. Những hàm đặc biệt này được gọi là Callback, là các hàm dev tự thiết kế và đăng ký nó với ESP-BLE-MESH stack

Trong ví dụ này, chúng ta cần đăng ký và xử lý 3 loại sự kiện chính:

1. Callback Provisioning (example_ble_mesh_provisioning_cb)

Đây là nơi cho phép Node gia nhập mạng. Hàm này quản lý các sự kiện liên quan đến quá trình kết nối ban đầu, được đăng ký với stack thông qua hàm: esp_ble_mesh_register_prov_callback()


/* -------------------------------------------------------------------------- */
/* Callback xử lý sự kiện Provisioning (Quan trọng)                          */
/* Hàm này được Stack gọi tự động khi có thay đổi về trạng thái nhập mạng.   */
/* -------------------------------------------------------------------------- */
static void example_ble_mesh_provisioning_cb(esp_ble_mesh_prov_cb_event_t event,
                                             esp_ble_mesh_prov_cb_param_t *param)
{
    // Dùng switch để phân loại sự kiện nhận được
    switch (event) {
    
    /* === CASE 1: NHẬP MẠNG THÀNH CÔNG (Sự kiện quan trọng nhất) === */
    // Node đã nhận được NetKey và Unicast Address từ Provisioner.
    case ESP_BLE_MESH_NODE_PROV_COMPLETE_EVT:
        ESP_LOGI(TAG, "--- PROVISIONING COMPLETE (Đã gia nhập mạng) ---");
        
        // In ra các thông tin 'hộ khẩu' mới được cấp.
        // Truy cập dữ liệu thông qua struct: param->node_prov_complete
        ESP_LOGI(TAG, "--> NetKey Index: 0x%04x", param->node_prov_complete.net_idx);
        ESP_LOGI(TAG, "--> Assigned Address (Unicast): 0x%04x", param->node_prov_complete.addr); // <-- Lưu ý địa chỉ này!
        ESP_LOGI(TAG, "--> Flags: 0x%02x", param->node_prov_complete.flags);
        ESP_LOGI(TAG, "--> IV Index: 0x%08x", param->node_prov_complete.iv_index);
        break;

    /* === CASE 2: NODE BỊ RESET === */
    // Node bị đá khỏi mạng hoặc được lệnh reset cục bộ. 
    // Nó sẽ xóa hết key và quay về trạng thái chờ provisioning.
    case ESP_BLE_MESH_NODE_PROV_RESET_EVT:
        ESP_LOGI(TAG, "--- Node Reset (Đã rời mạng) ---");
        break;

    /* Các sự kiện khác (ví dụ: Bắt đầu link, Gửi/Nhận PDU...) */
    // Tạm thời bỏ qua trong ví dụ cơ bản này.
    default:
        break;
    }
}

Thực tế, quá trình Provisioning diễn ra với hàng loạt sự kiện liên tiếp nhau. Để bắt và xử lý từng sự kiện này, chúng ta dùng cấu trúc switch-case. Tuy nhiên, với người mới bắt đầu, chúng ta không cần lo lắng về tất cả chúng. Chúng ta chỉ cần quan tâm đến kết quả cuối cùng: Thành công hoặc Reset.

Khi case ESP_BLE_MESH_NODE_PROV_COMPLETE_EVT xuất hiện, nghĩa là Node đã chính thức gia nhập mạng. Lúc này, hãy nhìn vào struct param->node_prov_complete, nó chứa toàn bộ 'giấy tờ tùy thân' mới của Node:

  • net_idx (Network Key Index): ID của chìa khóa mạng (thường là 0).
  • addr (Unicast Address): Đây là 'số nhà' độc nhất của Node (vd: 0x0002). Bạn buộc phải nhớ số này để biết mình đang điều khiển thiết bị nào.
  • flags & iv_index: Các cờ bảo mật đi kèm.

Bằng cách in (log) các thông số này ra Terminal, chúng ta có thế thấy rằng : 'À, Node này đã vào mạng ngon lành với địa chỉ X rồi!'"

2.Callback Config Server (example_ble_mesh_config_server_cb)

Trong ứng dụng cơ bản này, chúng ta sử dụng hàm này chủ yếu để theo dõi xem quá trình Binding AppKey có thành công hay không

/**
 * @brief Callback xử lý các sự kiện liên quan đến Configuration Server Model.
 *
 * Hàm này được gọi khi Provisioner gửi các lệnh cấu hình (như thêm AppKey,
 * bind AppKey vào Model) xuống node thiết bị.
 *
 * @param event Loại sự kiện xảy ra.
 * @param param Tham số đi kèm sự kiện.
 */
static void example_ble_mesh_config_server_cb(esp_ble_mesh_cfg_server_cb_event_t event,
                       esp_ble_mesh_cfg_server_cb_param_t *param)
{
  // Kiểm tra xem sự kiện có phải là thay đổi trạng thái (State Change) hay không
  if (event == ESP_BLE_MESH_CFG_SERVER_STATE_CHANGE_EVT) {

    // Kiểm tra Opcode (mã lệnh) nhận được từ Provisioner
    switch (param->ctx.recv_op) {

    /* Trường hợp 1: Nhận lệnh thêm AppKey (Application Key) thành công */
    case ESP_BLE_MESH_MODEL_OP_APP_KEY_ADD:
      ESP_LOGI(TAG, "AppKey Added Successfully!");
      ESP_LOGI(TAG, "NetIdx: 0x%04x, AppIdx: 0x%04x",
           param->value.state_change.appkey_add.net_idx,
           param->value.state_change.appkey_add.app_idx);
      break;

    /* Trường hợp 2: Nhận lệnh Bind (ràng buộc) AppKey vào một Model thành công */
    case ESP_BLE_MESH_MODEL_OP_MODEL_APP_BIND:
      ESP_LOGI(TAG, "Model Bound Successfully!");
      ESP_LOGI(TAG, "Elem Addr: 0x%04x, Model ID: 0x%04x, AppIdx: 0x%04x",
           param->value.state_change.mod_app_bind.element_addr,
           param->value.state_change.mod_app_bind.model_id,
           param->value.state_change.mod_app_bind.app_idx);
      break;

    /* Các trường hợp opcode khác (nếu cần xử lý thêm) */
    default:
      // Sử dụng PRIx32 để format in log cho biến kiểu uint32_t/opcode an toàn
      ESP_LOGW(TAG, "Unknown Config Opcode received: 0x%04" PRIx32,
           param->ctx.recv_op);
      break;
    }
  }
}

Ở đoạn code trên, chúng ta tập trung bắt sự kiện ESP_BLE_MESH_CFG_SERVER_STATE_CHANGE_EVT. Sự kiện này cực kỳ quan trọng vì nó chỉ được kích hoạt khi Node đã nhận và áp dụng thành công một thay đổi cấu hình nào đó từ Provisioner, và dựa vào opcode mà stack trả về, và xử lí từng sự kiện đó.

3. Callback Generic OnOff Server (example_ble_mesh_generic_server_cb)

Sau khi đã có chìa khóa ứng dụng (AppKey) và luật lệ rõ ràng, chúng ta cần một người trực tiếp thực hiện công việc, thì hàm callback này chính là nơi quyết định tính năng cốt lõi của sản phẩm: Điều khiển Bật/Tắt đèn LED.

Hàm này sẽ được gọi ngay lập tức khi Node nhận được lệnh điều khiển (SET) từ điện thoại gửi xuống.

/**
 * @brief Callback xử lý các sự kiện của Generic OnOff Server.
 *
 * Hàm này nhận lệnh từ Client (như công tắc hoặc ứng dụng điện thoại)
 * để thay đổi trạng thái On/Off thực tế của thiết bị (ví dụ: bật/tắt LED).
 *
 * @param event Loại sự kiện.
 * @param param Tham số đi kèm sự kiện.
 */
static void example_ble_mesh_generic_server_cb(esp_ble_mesh_generic_server_cb_event_t event,
                        esp_ble_mesh_generic_server_cb_param_t *param)
{
  // Kiểm tra sự kiện thay đổi trạng thái (State Change)
  if (event == ESP_BLE_MESH_GENERIC_SERVER_STATE_CHANGE_EVT) {

    // Kiểm tra Opcode: Cần xử lý cả 2 trường hợp:
    // 1. SET: Lệnh yêu cầu phản hồi (Acknowledged)
    // 2. SET_UNACK: Lệnh không cần phản hồi (Unacknowledged) - giúp giảm tải mạng
    if (param->ctx.recv_op == ESP_BLE_MESH_MODEL_OP_GEN_ONOFF_SET ||
      param->ctx.recv_op == ESP_BLE_MESH_MODEL_OP_GEN_ONOFF_SET_UNACK) {

      // Lấy giá trị On/Off từ gói tin (0: Off, 1: On)
      uint8_t state = param->value.state_change.onoff_set.onoff;

      // In log để debug
      ESP_LOGI(TAG, "LED State Change: %d", state);

      // Điều khiển phần cứng thực tế (GPIO)
      // Lưu ý: Đảm bảo LED_GPIO đã được cấu hình output trước đó
      gpio_set_level(LED_GPIO, state);
    }
  }
}

Tương tự như hai hàm trên, chúng ta cũng lọc các sự kiện để tìm ra đúng thời điểm cần hành động:

1. Bắt sự kiện (ESP_BLE_MESH_GENERIC_SERVER_STATE_CHANGE_EVT)

  • Ý nghĩa: Sự kiện này xảy ra khi trạng thái nội tại của Model thay đổi. Điều này xảy ra sau khi Mesh Stack đã nhận gói tin từ mạng, giải mã thành công bằng AppKey và xác nhận dữ liệu hợp lệ.
  • Tại sao lại là State Change? Thay vì xử lý gói tin thô (raw data), Mesh Stack của ESP-IDF đã làm giúp chúng ta phần khó nhất. Nó tự động cập nhật trạng thái của Model, và việc của chúng ta chỉ là phản ứng lại sự thay đổi đó.

2. Kiểm tra Lệnh (SET và SET_UNACK)

  • Chúng ta kiểm tra ctx.recv_op để đảm bảo đây là lệnh Đặt trạng thái (SET).
  • Lưu ý: Có 2 loại lệnh SET:
  • SET: Có gửi xác nhận (Reliable).
  • SET_UNACK: Không cần xác nhận (Unreliable - Nhanh hơn).
  • Code trên xử lý cả hai trường hợp để đảm bảo dù người dùng gửi kiểu gì thì đèn cũng sáng.

3. Thực thi hành động (gpio_set_level)

  • Đây là đích đến cuối cùng của chúng ta. Giá trị người dùng muốn (Bật = 1, Tắt = 0) nằm gọn trong biến param->value.state_change.onoff_set.onoff.
  • Chúng ta chỉ việc lấy nó ra và truyền vào hàm điều khiển GPIO để bật/tắt bóng đèn vật lý.


Vậy là xong! Chỉ với 3 hàm Callback này, chúng ta đã bao quát toàn bộ vòng đời của một thiết bị Mesh: Từ lúc Gia nhập (Provisioning), Cấu hình (Config) cho đến lúc Hoạt động (Generic OnOff). Việc còn lại chỉ là đăng ký chúng trong hàm app_main và biên dịch code.

V, Hướng dẫn chạy và giao tiếp với mạng Bluetooth mesh sử dụng app nRF Mesh.

Code đã xong, biên dịch đã thành công. Chúng ta sẽ sử dụng điện thoại thông minh để đóng vai trò là Provisioner (người quản lý), thiết lập mạng lưới, cũng như gửi lệnh onoff tới node của chúng ta.

Chúng ta sẽ sử dụng ứng dụng nRF Mesh (của Nordic Semiconductor). Đây là ứng dụng chuẩn mực và ổn định nhất để test Bluetooth Mesh hiện nay (có sẵn trên cả iOS và Android).

Link tải tại đây : https://www.nordicsemi.com/Products/Development-tools/nRF-Mesh

Sau khi tải xong, mở ứng dụng.

Giờ sang ESP, build code và nạp vào như những blog trước :

Sau khi nạp thành công và mở monitor, thấy được node Ble mesh đã được cài đặt, và đang phát tín hiệu để được provision.

Trên ứng dụng nRF mesh, vào dấu cộng ở góc phải màn hình :

Ấn vào ESP-BLE-MESH để thêm node

Ấn vào IDENTIFY để ứng dụng xác nhận thiết bị

Ấn vào provision để bắt đầu quá trình

Quá trình hoàn tất, khi nhìn vào Monitor sẽ thấy ESP báo đã provision thành công và hiển thị các thông tin về mạng mesh.

Trên ứng dụng nhấn vào node chúng ta vừa provision :

Có thể thấy node đang có 1 element với 2 modal, đúng như chúng ta code :

Để có thể gửi bản tin on off tới modal Generic onoff server, chúng ta phải bind appkey cho model này bằng cách ấn vào model đó.

Đồng thời monitor sẽ log bind key thành công với những thông tin mà chúng ta đã code

Trên ứng dụng lướt xuống dưới, nhấn on để bật tắt led trên board ESP32 của chúng ta.

Đèn ESP32 đã sáng, vậy là anh em đã thành công rồi đó.

Vậy là anh em chúng ta đã cùng nhau đi hết một chặng đường khá dài: Từ việc tạo một Project trắng tinh, cấu hình menuconfig tự tay định nghĩa từng dòng Composition Data, viết từng hàm Callback.

Qua bài viết này, mình hy vọng anh em không chỉ biết cách "chạy code" mà còn thực sự hiểu code. Chúng ta đã nắm được:

  • Cách khởi tạo Stack Bluetooth Mesh chuẩn chỉ.
  • Cơ chế Event-driven và vai trò của các hàm Callback
  • Quy trình Provisioning & Binding thực tế trên ứng dụng nRF Mesh.

Đây chính là nền móng vững chắc nhất. Khi đã hiểu những điều cốt lõi này, việc anh em muốn thêm 5-10 Element, hay tích hợp thêm cảm biến, relay vào Node sẽ trở nên dễ dàng hơn rất nhiều.

Nhưng khoan, Bluetooth Mesh mà chỉ 1 Node thì khác gì Bluetooth thường đâu nhỉ? Sức mạnh thực sự của Mesh nằm ở khả năng kết nối và điều khiển hàng loạt!

Trong bài blog tiếp theo, chúng ta sẽ làm những ứng dụng mạnh mẽ hơn nhiều:

  • Group Subscription: Tạo một nhóm "Living Room" và điều khiển 3-4 con ESP32 bật tắt đồng thời chỉ bằng 1 nút nhấn.
  • Publication: Để các Node tự động báo cáo trạng thái định kỳ.
  • Client Node: Biến một con ESP32 khác thành cái Remote để điều khiển đèn

Happy Coding & Hẹn gặp lại anh em đồng nhúng!