Giao tiếp Node-to-Node: Xây dựng Generic OnOff Client

Hello anh em đồng nhúng !

Ở bài blog trước (Bài 3), chúng ta đã cùng nhau xây dựng thành công một Node Server và điều khiển nó cực kỳ mượt mà, thông qua ứng dụng nRF Mesh trên điện thoại. Cảm giác nhấn nút trên màn hình cảm ứng và thấy đèn LED trên board sáng lên chắc chắn là một khởi đầu đầy hứng khởi.

Tuy nhiên, hãy nhìn vào thực tế. Trong một hệ thống nhà thông minh (Smart Home) hay công nghiệp (Industrial IoT), chúng ta không phải lúc nào cũng muốn điều khiển bằng điện thoại. Chúng ta cần những công tắc vật lý gắn tường, những cảm biến tự động gửi lệnh khi phát hiện chuyển động. Nói cách khác, các thiết bị cần phải tự giao tiếp được với nhau mà không cần sự can thiệp liên tục của con người hay smarthome.

Đó chính là lý do hôm nay chúng ta sẽ nâng cấp hệ thống: Biến một board ESP32 thứ hai thành một cái remote (Client Node) để điều khiển nhiều board Server Node.


I, Khái niệm Client Model & kịch bản xây dựng.

Trước khi vào bài, cùng ôn lại một số kiến thức nhé :


  1. Server Model : Là thiết bị “bị động” chờ lệnh, chứa trạng thái (On/Off) và là nơi thực thi các lệnh nhận được và báo cáo lại.
  2. Client Model : Nằm tại thiết bị “chủ động”, không chứa trạng thái (On/Off), nhiệm vụ là gửi lệnh để thay đổi trạng thái của server model.
  3. Publication : Hành động gửi tin đi về 1 địa chỉ nào đó ( Unicast address, Group address)

Để chứng minh sức mạnh của giao tiếp Node-to-Node trong mạng Bluetooth mesh, kịch bản hôm nay của anh em ta sẽ gồm:


1.1. Mục tiêu

- Lập trình ra node chứa Generic OnOff Client model để điều khiển các node chạy OnOff Server model.

=> Khi bấm nút trên board Client -> Gửi lệnh điều khiển qua mạng Bluteooth mesh tới Node Server-> Node server nhận lệnh và bật tắt đèn trên board.

- App NRF Mesh vẫn được sử dụng để provision, nhưng không dùng để điều khiển nữa.


1.2. Chuẩn bị phần cứng

Cần ít nhất 02 board ESP32, hoặc nhiều hơn (tùy độ giàu :>)


  • Board A (server ) : Đóng vai trò server model.

Trong bài này mình sẽ sử dụng Board ESP32 tới từ E-LAB :

  • Board B ( Client) : Chạy firmware on/off client của chúng ta sắp viết ( đóng vai trò điều khiển server).

Bạn có thể sử dụng bất kỳ board ESP32 nào có hỗ trợ BLE Mesh, mình sẽ sử dụng board esp32c3 super mini cho ví dụ này.


II, Coding & triển khai

Source Code mẫu :


Về cơ bản, khung chương trình (khởi tạo NVS, Bluetooth Controller, Mesh Stack...) của Node Client giống hệt Node Server ở bài blog số 3 mà các blog trước đã giải thích rất chi tiết rồi. Sự khác biệt nằm ở model chúng ta khai báo và các hàm callback xử lý sự kiện.

Mọi người có thể copy code từ project trước và thay đổi những điểm chính sau.


2.1. Khai báo Model Client

Ở model server ta có 1 biến struct quản lí là : esp_ble_mesh_gen_on_off_srv_t onoff_server

Cần thay đổi biến này để set về Client model với :


static esp_ble_mesh_client_t onoff_client;

Ở Model Client chúng ta sẽ thay thế marco ESP_BLE_MESH_MODEL_GEN_ONOFF_SRV (Định nghĩa cho model server)

bằng macro ESP_BLE_MESH_MODEL_GEN_ONOFF_CLI để định nghĩa cho model client, đổi đối số truyền vào là &onoff_client


static esp_ble_mesh_model_t root_models[] = {
 ESP_BLE_MESH_MODEL_CFG_SRV(&config_server),
 ESP_BLE_MESH_MODEL_GEN_ONOFF_CLI(&onoff_cli_pub, &onoff_client),
};

2.2, Viết hàm gửi lệnh điều khiển (“The mesh control”)

Đây sẽ là phần quan trọng nhất của bài này, chúng ta sẽ thiết kế 1 hàm mà khi nhấn nút tại Node chứa model client, hàm này sẽ đóng gói bản tin chuẩn Bluetooth mesh và bắn nó vào không gian mạng.


Trong ESP-IDF, để gửi một lệnh điều khiển dạng Generic OnOff, chúng ta sử dụng API:

esp_ble_mesh_generic_client_set_state().

Anh em có thể tham khảo đoạn code dưới đây :

/* Hàm gửi lệnh Bật/Tắt (Generic OnOff Set) */
void example_ble_mesh_send_gen_onoff_set(void)
{
    esp_ble_mesh_generic_client_set_state_t set = {0};
    esp_ble_mesh_client_common_param_t common = {0};
    esp_err_t err;

    // ---------------------------------------------------------
    // 1. Lấy con trỏ đến Model Client
    // Lưu ý: Đảm bảo index [1] trỏ đúng vào GEN_ONOFF_CLI trong mảng root_models
    // ---------------------------------------------------------
    esp_ble_mesh_model_t *model = &root_models[1]; 

    // ---------------------------------------------------------
    // 2. Cấu hình các tham số chung (Common Parameters)
    // ---------------------------------------------------------
    common.opcode = ESP_BLE_MESH_MODEL_OP_GEN_ONOFF_SET_UNACK; // Dùng UNACK để không cần chờ phản hồi (nhanh hơn)
    common.model = model;
    
    // Thiết lập Key bảo mật
    common.ctx.net_idx = 0x0000; // Primary NetKey
    common.ctx.app_idx = 0x0000; // Primary AppKey

    // Thiết lập địa chỉ đích
    common.ctx.addr = 0xFFFF;    // 0xFFFF = Broadcast Address (Gửi cho tất cả node trong mạng)
    
    // Thiết lập truyền tin
    common.ctx.send_ttl = 3;     // Time To Live: Gói tin được relay tối đa 3 lần
    common.msg_timeout = 0;      // Timeout = 0 vì dùng bản tin Unacknowledged

    // ---------------------------------------------------------
    // 3. Cấu hình dữ liệu trạng thái (State Data)
    // ---------------------------------------------------------
    // Biến static để lưu trạng thái giữa các lần gọi hàm
    static uint8_t toggle_val = 0;
    toggle_val = !toggle_val;    // Đảo trạng thái: 0 -> 1 -> 0...

    set.onoff_set.onoff = toggle_val; // Giá trị: 1 (Bật) hoặc 0 (Tắt)
    set.onoff_set.op_en = false;      // false: Bỏ qua tham số Transition Time/Delay

    // ---------------------------------------------------------
    // 4. Gửi lệnh đi (Execute)
    // ---------------------------------------------------------
    err = esp_ble_mesh_generic_client_set_state(&common, &set);
    
    if (err == ESP_OK) {
        ESP_LOGI(TAG, "Gửi lệnh thành công! Trạng thái: %s", toggle_val ? "ON" : "OFF");
    } else {
        ESP_LOGE(TAG, "Gửi lệnh thất bại, mã lỗi: %d", err);
    }
}

Cấu trúc dữ liệu quan trọng :

Opcode :

ESP_BLE_MESH_MODEL_OP_GEN_ONOFF_SET_UNAC.

Opcode này báo cho stack biết rằng bản tin không yêu cầu node đích phản hồi, giúp giảm trễ, băng thông, phù hợp cho việc điều khiển thời gian thực

Ngược lại có thể sử dụng Opcode : ESP_BLE_MESH_MODEL_OP_GEN_ONOFF_SET.

Opcode này buộc Node client chờ phản hồi từ Server, nếu không nhận được trong khoảng thời gian msg_timeout sẽ báo lỗi, như vậy đổi lại được sự tin cậy cao hơn

1. esp_ble_mesh_client_common_param_t

Đây là cấu trúc định nghĩa CÁCH tin nhắn được gửi đi. Nó chứa các thông tin về định tuyến, bảo mật và ngữ cảnh mạng.

typedef struct {
  esp_ble_mesh_model_t *model; /*!< Con trỏ tới Model Client đang gửi tin (Người gửi là ai?) */  
  uint32_t opcode; /*!< Mã lệnh (Opcode) (Ví dụ: SET hay SET_UNACK?) */  
  esp_ble_mesh_msg_ctx_t ctx; /*!< Ngữ cảnh tin nhắn (Địa chỉ đích, Key, TTL...) */  
  int32_t msg_timeout; /*!< Thời gian chờ phản hồi (ms) */  
  uint8_t msg_role; /*!< Vai trò (Thường là ROLE_NODE) */
} esp_ble_mesh_client_common_param_t;

2. esp_ble_mesh_generic_client_set_state_t

Đây là cấu trúc định nghĩa nội dung muốn gửi, vì các model Generic có thể điều khiển nhiều thứ ( Bật tắt, Pin, level...) nên cấu trúc này là 1 union(tập hợp) chứa nhiều struct con.

typedef union {
esp_ble_mesh_gen_onoff_set_t onoff_set; /*!< Dùng cho lệnh Bật/Tắt (Chúng ta dùng cái này) */
esp_ble_mesh_gen_level_set_t level_set; /*!< Dùng cho lệnh chỉnh mức độ (Level) */
esp_ble_mesh_gen_def_trans_time_set_t def_trans_time_set; /*!< Dùng cho lệnh chỉnh thời gian chuyển đổi */
// ... và nhiều loại khác ...
} esp_ble_mesh_generic_client_set_state_t;

Vì chúng ta đang làm bài toán bật tắt (On/off), chúng ta chỉ quan tâm đến struct onoff_set. Bên trong onoff_set có :

onoff: Giá trị cốt lõi (1 = Bật, 0 = Tắt).

op_en (Optional Parameters Enabled): Cờ cho phép sử dụng tham số phụ.

  • Nếu false: Đèn bật/tắt ngay lập tức theo cài đặt mặc định.
  • Nếu true: Bạn được phép chỉnh thêm trans_time (thời gian đèn sáng dần lên) và delay (độ trễ trước khi bật).

* Hàm quan trọng :

esp_ble_mesh_generic_client_set_state(esp_ble_mesh_client_common_param_t*params, esp_ble_mesh_generic_client_set_state_t *set_state).

Chức năng : Hàm này thực hiện một chuỗi các nhiệm vụ phức tạp bên dưới lớp ứng dụng :

  1. Đóng gói dữ liệu: Lấy dữ liệu từ set_state và định dạng nó theo chuẩn Generic OnOff Model.
  2. Mã hóa & Bảo mật: Sử dụng AppKeyNetKey từ params để mã hóa gói tin, đảm bảo chỉ các thiết bị trong mạng mới đọc được.
  3. Định tuyến: Dựa vào địa chỉ đích (ctx.addr) và TTL trong params để quyết định cách gửi gói tin đi (Unicast hay Multicast/Group).
  4. Gửi đi: Đẩy gói tin xuống tầng dưới để phát sóng ra môi trường.

Các tham số :

params (Input/Output):

  • Là con trỏ trỏ đến struct esp_ble_mesh_client_common_param_t mà chúng ta đã phân tích ở trên.
  • Nó chứa thông tin: Gửi cho ai (addr), gửi lệnh gì (opcode), dùng chìa khóa nào (net_idx, app_idx).

set_state (Input):

  • Là con trỏ trỏ đến struct esp_ble_mesh_generic_client_set_state_t
  • Nó chứa giá trị thực tế bạn muốn cài đặt cho Server (ví dụ: onoff = 1 để bật đèn).

III, Build & flash, config and run

Vì mặc định, ESP-IDF tắt các tính năng Client Model để tiết kiệm bộ nhớ, nên cần phải vào menuconfig để bật lại.

Phần cấu hình của client model nằm ở :

Component config ---> ESP BLE Mesh Support --> Support for BLE Mesh Client/Server models( ở gần cuối menu)

Trong list nhấn enter hoặc space để bật [ ] Generic OnOff Client model.

Save lại menuconfig và build & flash code thôi

Sau khi flash thành công, stack BLE Mesh đã được khởi tạo, hãy vào app NRF Mesh để provision cho thiết bị, cho phép Node client của chúng ta gia nhập vào mạng mesh (Quá trình giống ở Blog trước).

Lưu ý : phải bind chung appkey cho 2 model client và server trên 2 node.

Ở trong code, chúng ta đã cài sẵn địa chỉ gửi là 0xFFFF. Đây giống như việc dùng loa phóng thanh thay vì gọi điện thoại riêng: Công tắc sẽ gửi lệnh cho tất cả mọi thiết trong mạng cùng nghe. Cứ có chung AppKey là nhận được hết !

Flash lại firmware ở bài 3 cho Node server provision và bind appkey giống với Node Client.

Sau khi quá trình hoàn thành, bấm nút boot trên board Client để thử nghiệm :

Khi chắc chắn đã bind appkey tương ứng cho cả 2 board, khi bấm nút tại Client board (Esp32c3) ta sẽ thấy đèn trên Server board được đảo trạng thái sau mỗi lần nhấn :


Qua bài blog này, mình cũng đã giới thiệu đến anh em kiến thức nền tảng về hai thực thể quan trọng nhất trong mạng lưới:

  • Server Model: Đơn vị chấp hành và lưu trữ trạng thái.
  • Client Model: Đơn vị điều phối và phát lệnh điều khiển.

Nhưng ở blog này mới chỉ demo 2 node nói chuyện với nhau, trong một mạng lưới thực tế với hàng trăm thiết bị, việc quản lý luồng tin nhắn không thể đơn giản như vậy. Làm thế nào để công tắc phòng khách chỉ bật đèn phòng khách? Làm sao để một cảm biến kích hoạt được một nhóm thiết bị cụ thể mà không ảnh hưởng đến các vùng khác?

Câu trả lời nằm ở nghệ thuật quản lý không gian địa chỉ (Addressing).

Trong bài blog tiếp theo, chúng ta sẽ đi sâu vào một chủ đề mang tính "xương sống" của hệ thống: Các loại địa chỉ trong Bluetooth Mesh (Unicast, Group, Virtual) và Kỹ thuật ứng dụng linh hoạt vào Code.

Hẹn gặp lại anh em ở bài viết tới để cùng nhau đào sâu về Bluetooth mesh trong ESP-IDF nhé.

Happy Coding!