Phát triển bằng API OpenThread

1. Giới thiệu

26b7f4f6b3ea0700.pngS.

OpenThread do Nest phát hành là một phương thức triển khai nguồn mở của giao thức mạng Thread®. Nest đã ra mắt OpenThread để cung cấp rộng rãi công nghệ dùng trong các sản phẩm của Nest cho các nhà phát triển nhằm đẩy nhanh quá trình phát triển sản phẩm dành cho nhà thông minh.

Thông số kỹ thuật Thread xác định giao thức giao tiếp giữa các thiết bị không dây đáng tin cậy, an toàn và tiết kiệm năng lượng dựa trên IPv6 cho các ứng dụng trong nhà. OpenThread thực hiện tất cả các lớp mạng Thread bao gồm IPv6, 6LoWPAN, IEEE 802.15.4 với bảo mật MAC, thành lập liên kết lưới và định tuyến lưới.

Trong lớp học lập trình này, bạn sẽ sử dụng OpenThread API để khởi động một mạng Thread, giám sát và phản ứng với các thay đổi về vai trò trên thiết bị, gửi thông báo UDP, đồng thời liên kết các thao tác này với các nút và đèn LED trên phần cứng thực.

2a6db2e258c32237.pngS

Kiến thức bạn sẽ học được

  • Cách lập trình các nút và đèn LED trên bảng nhà phát triển Bắc Âu nRF52840
  • Cách sử dụng các API OpenThread phổ biến và lớp otInstance
  • Cách theo dõi và phản ứng với các thay đổi về trạng thái OpenThread
  • Cách gửi thông báo UDP đến tất cả thiết bị trong mạng Thread
  • Cách sửa đổi Makefile

Bạn cần có

Phần cứng:

  • 3 Bảng phát triển chất bán dẫn Bắc Âu nRF52840
  • 3 cáp USB sang Micro-USB để kết nối các bo mạch
  • Một máy Linux có ít nhất 3 cổng USB

Phần mềm:

  • Chuỗi công cụ GNU
  • Công cụ dòng lệnh nRF5x của Bắc Âu
  • Phần mềm Segger J-Link
  • OpenThread
  • Git

Ngoại trừ có thông báo khác, nội dung của Lớp học lập trình này được cấp phép theo Giấy phép Creative Commons Attribution 3.0 và các mã mẫu được cấp phép theo Giấy phép Apache 2.0.

2. Bắt đầu

Hoàn thành Lớp học lập trình về phần cứng

Trước khi bắt đầu lớp học lập trình này, bạn nên hoàn tất Lớp học lập trình Xây dựng mạng Thread bằng nRF52840 Bảng mạch và OpenThread. Lớp học này:

  • Chi tiết tất cả phần mềm bạn cần để tạo và cài đặt ROM
  • Hướng dẫn bạn cách xây dựng OpenThread và cài đặt ROM trên các bảng nRF52840 của Bắc Âu
  • Minh hoạ thông tin cơ bản về mạng Thread

Không cần thiết lập môi trường nào để xây dựng OpenThread và cài đặt ROM bảng, trong đó chỉ trình bày chi tiết về việc cài đặt ROM trong lớp học lập trình này. Đây chỉ là những hướng dẫn cơ bản để cài đặt ROM bảng. Giả định rằng bạn đã hoàn thành Lớp học lập trình về Xây dựng mạng Thread.

Máy Linux

Lớp học lập trình này được thiết kế để sử dụng máy Linux dựa trên nền tảng i386 hoặc x86 để cài đặt ROM tất cả các bo mạch phát triển Thread. Tất cả các bước đã được kiểm tra trên Ubuntu 14.04.5 LTS (Trusty Tahr).

Bảng bán dẫn Bắc Âu nRF52840

Lớp học lập trình này sử dụng 3 bảng nRF52840 PDK.

a6693da3ce213856.png

Cài đặt phần mềm

Để tạo và cài đặt ROM OpenThread, bạn cần cài đặt SEGGER J-Link, các công cụ Dòng lệnh nRF5x, ARM GNU Toolchain và nhiều gói Linux khác nhau. Nếu đã hoàn thành Lớp học lập trình về Xây dựng mạng Thread theo yêu cầu, bạn sẽ có mọi thứ cần thiết. Nếu chưa, hãy hoàn thành lớp học lập trình đó trước khi tiếp tục để đảm bảo rằng bạn có thể xây dựng và cài đặt ROM OpenThread vào các bảng phát triển nRF52840.

3. Sao chép kho lưu trữ

OpenThread đi kèm với mã xử lý ứng dụng mẫu mà bạn có thể sử dụng làm điểm bắt đầu cho Lớp học lập trình này.

Sao chép kho lưu trữ các ví dụ OpenThread Nordic nRF528xx và tạo OpenThread:

$ git clone --recursive https://github.com/openthread/ot-nrf528xx
$ cd ot-nrf528xx
$ ./script/bootstrap

4. Kiến thức cơ bản về API OpenThread

Các API công khai của OpenThread nằm tại ./openthread/include/openthread trong kho lưu trữ OpenThread. Các API này cung cấp quyền truy cập vào nhiều tính năng và chức năng của OpenThread ở cả cấp độ Thread và nền tảng để sử dụng trong các ứng dụng của bạn:

  • Chế độ điều khiển và thông tin về đối tượng OpenThread
  • Các dịch vụ ứng dụng như IPv6, UDP và CoAP
  • Quản lý thông tin đăng nhập mạng, cùng với vai trò Ủy viên và Người tham gia
  • Quản lý Bộ định tuyến biên
  • Các tính năng nâng cao như Giám sát trẻ em và Phát hiện Jam

Thông tin tham chiếu về tất cả API OpenThread có tại openthread.io/reference.

Sử dụng API

Để sử dụng API, hãy đưa tệp tiêu đề của API đó vào một trong các tệp ứng dụng của bạn. Sau đó, hãy gọi hàm mong muốn.

Ví dụ: ứng dụng mẫu CLI đi kèm với OpenThread sử dụng các tiêu đề API sau:

./openthread/examples/apps/cli/main.c

#include <openthread/config.h>
#include <openthread/cli.h>
#include <openthread/diag.h>
#include <openthread/tasklet.h>
#include <openthread/platform/logging.h>

Thực thể OpenThread

Cấu trúc otInstance là cấu trúc mà bạn sẽ thường xuyên dùng khi làm việc với các API OpenThread. Sau khi được khởi tạo, cấu trúc này đại diện cho một thực thể tĩnh của thư viện OpenThread và cho phép người dùng thực hiện các lệnh gọi OpenThread API.

Ví dụ: thực thể OpenThread được khởi tạo trong hàm main() của ứng dụng mẫu CLI:

./openthread/examples/apps/cli/main.c

int main(int argc, char *argv[])
{
    otInstance *instance

...

#if OPENTHREAD_ENABLE_MULTIPLE_INSTANCES
    // Call to query the buffer size
    (void)otInstanceInit(NULL, &otInstanceBufferLength);

    // Call to allocate the buffer
    otInstanceBuffer = (uint8_t *)malloc(otInstanceBufferLength);
    assert(otInstanceBuffer);

    // Initialize OpenThread with the buffer
    instance = otInstanceInit(otInstanceBuffer, &otInstanceBufferLength);
#else
    instance = otInstanceInitSingle();
#endif

...

    return 0;
}

Các hàm dành riêng cho nền tảng

Nếu bạn muốn thêm các hàm dành riêng cho nền tảng vào một trong các ứng dụng mẫu có trong OpenThread, trước tiên, hãy khai báo các hàm đó trong tiêu đề ./openthread/examples/platforms/openthread-system.h bằng cách sử dụng không gian tên otSys cho tất cả các hàm. Sau đó, hãy triển khai các phương pháp này trong tệp nguồn dành riêng cho nền tảng. Được tóm tắt theo cách này, bạn có thể sử dụng cùng tiêu đề hàm cho các nền tảng mẫu khác.

Ví dụ: bạn phải khai báo các hàm GPIO mà chúng ta sẽ sử dụng để nối vào các nút và đèn LED của nRF52840 trong openthread-system.h.

Mở tệp ./openthread/examples/platforms/openthread-system.h trong trình chỉnh sửa văn bản bạn muốn dùng.

./openthread/examples/platforms/openthread-system.h

HÀNH ĐỘNG: Thêm nội dung khai báo hàm GPIO dành riêng cho nền tảng.

Thêm các nội dung khai báo hàm này sau #include cho tiêu đề openthread/instance.h:

/**
 * Init LED module.
 *
 */
void otSysLedInit(void);
void otSysLedSet(uint8_t aLed, bool aOn);
void otSysLedToggle(uint8_t aLed);

/**
* A callback will be called when GPIO interrupts occur.
*
*/
typedef void (*otSysButtonCallback)(otInstance *aInstance);
void otSysButtonInit(otSysButtonCallback aCallback);
void otSysButtonProcess(otInstance *aInstance);

Chúng ta sẽ triển khai các tính năng này trong bước tiếp theo.

Xin lưu ý rằng việc khai báo hàm otSysButtonProcess sử dụng otInstance. Bằng cách đó, ứng dụng có thể truy cập vào thông tin về thực thể OpenThread khi nhấn một nút, nếu cần. Tất cả phụ thuộc vào nhu cầu ứng dụng của bạn. Nếu không cần hàm này trong quá trình triển khai hàm, bạn có thể sử dụng macro OT_UNUSED_VARIABLE của OpenThread API để loại bỏ các lỗi bản dựng xung quanh các biến không dùng đến cho một số chuỗi công cụ. Chúng ta sẽ xem các ví dụ về phương thức này ở phần sau.

5. Triển khai việc trừu tượng hoá nền tảng GPIO

Ở bước trước, chúng ta đã tìm hiểu nội dung khai báo hàm dành riêng cho nền tảng trong ./openthread/examples/platforms/openthread-system.h có thể dùng cho GPIO. Để truy cập các nút và đèn LED trên bo mạch chủ nRF52840, bạn cần triển khai các chức năng đó cho nền tảng nRF52840. Trong mã này, bạn sẽ thêm các hàm:

  • Khởi chạy các chế độ và chân GPIO
  • Điều khiển điện áp trên chân
  • Bật các ngắt GPIO và đăng ký lệnh gọi lại

Trong thư mục ./src/src, hãy tạo một tệp mới có tên gpio.c. Trong tệp mới này, hãy thêm nội dung sau đây.

./src/src/gpio.c (tệp mới)

HÀNH ĐỘNG: Thao tác Thêm định nghĩa.

Các định nghĩa này đóng vai trò là các thành phần trừu tượng giữa các giá trị cụ thể của nRF52840 và các biến được sử dụng ở cấp ứng dụng OpenThread.

/**
 * @file
 *   This file implements the system abstraction for GPIO and GPIOTE.
 *
 */

#define BUTTON_GPIO_PORT 0x50000300UL
#define BUTTON_PIN 11 // button #1

#define GPIO_LOGIC_HI 0
#define GPIO_LOGIC_LOW 1

#define LED_GPIO_PORT 0x50000300UL
#define LED_1_PIN 13 // turn on to indicate leader role
#define LED_2_PIN 14 // turn on to indicate router role
#define LED_3_PIN 15 // turn on to indicate child role
#define LED_4_PIN 16 // turn on to indicate UDP receive

Để biết thêm thông tin về các nút và đèn LED của nRF52840, hãy truy cập vào Trung tâm thông tin chất bán dẫn Bắc Âu.

HÀNH ĐỘNG: Thêm tiêu đề bao gồm.

Tiếp theo, hãy thêm tiêu đề bao gồm thông tin bạn sẽ cần cho chức năng GPIO.

/* Header for the functions defined here */
#include "openthread-system.h"

#include <string.h>

/* Header to access an OpenThread instance */
#include <openthread/instance.h>

/* Headers for lower-level nRF52840 functions */
#include "platform-nrf5.h"
#include "hal/nrf_gpio.h"
#include "hal/nrf_gpiote.h"
#include "nrfx/drivers/include/nrfx_gpiote.h"

HÀNH ĐỘNG: Thêm hàm gọi lại và hàm gián đoạn cho Nút 1.

Tiếp theo, hãy thêm mã này. Hàm in_pin1_handler là lệnh gọi lại được đăng ký khi khởi chạy chức năng nhấn nút (ở phần sau của tệp này).

Vui lòng lưu ý cách lệnh gọi lại này sử dụng macro OT_UNUSED_VARIABLE, vì các biến được chuyển đến in_pin1_handler không thực sự được dùng trong hàm.

/* Declaring callback function for button 1. */
static otSysButtonCallback sButtonHandler;
static bool                sButtonPressed;

/**
 * @brief Function to receive interrupt and call back function
 * set by the application for button 1.
 *
 */
static void in_pin1_handler(uint32_t pin, nrf_gpiote_polarity_t action)
{
    OT_UNUSED_VARIABLE(pin);
    OT_UNUSED_VARIABLE(action);
    sButtonPressed = true;
}

HÀNH ĐỘNG: Thêm một hàm để định cấu hình đèn LED.

Thêm mã này để định cấu hình chế độ và trạng thái của tất cả đèn LED trong quá trình khởi chạy.

/**
 * @brief Function for configuring: PIN_IN pin for input, PIN_OUT pin for output,
 * and configures GPIOTE to give an interrupt on pin change.
 */

void otSysLedInit(void)
{
    /* Configure GPIO mode: output */
    nrf_gpio_cfg_output(LED_1_PIN);
    nrf_gpio_cfg_output(LED_2_PIN);
    nrf_gpio_cfg_output(LED_3_PIN);
    nrf_gpio_cfg_output(LED_4_PIN);

    /* Clear all output first */
    nrf_gpio_pin_write(LED_1_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_2_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_3_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_4_PIN, GPIO_LOGIC_LOW);

    /* Initialize gpiote for button(s) input.
     Button event handlers are set in the application (main.c) */
    ret_code_t err_code;
    err_code = nrfx_gpiote_init();
    APP_ERROR_CHECK(err_code);
}

HÀNH ĐỘNG: Thêm một hàm để đặt chế độ của đèn LED.

Hàm này sẽ được dùng khi vai trò của thiết bị thay đổi.

/**
 * @brief Function to set the mode of an LED.
 */

void otSysLedSet(uint8_t aLed, bool aOn)
{
    switch (aLed)
    {
    case 1:
        nrf_gpio_pin_write(LED_1_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 2:
        nrf_gpio_pin_write(LED_2_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 3:
        nrf_gpio_pin_write(LED_3_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 4:
        nrf_gpio_pin_write(LED_4_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    }
}

HÀNH ĐỘNG: Thêm hàm để bật/tắt chế độ của đèn LED.

Hàm này sẽ được dùng để bật/tắt LED4 khi thiết bị nhận được thông báo UDP nhiều hướng.

/**
 * @brief Function to toggle the mode of an LED.
 */
void otSysLedToggle(uint8_t aLed)
{
    switch (aLed)
    {
    case 1:
        nrf_gpio_pin_toggle(LED_1_PIN);
        break;
    case 2:
        nrf_gpio_pin_toggle(LED_2_PIN);
        break;
    case 3:
        nrf_gpio_pin_toggle(LED_3_PIN);
        break;
    case 4:
        nrf_gpio_pin_toggle(LED_4_PIN);
        break;
    }
}

HÀNH ĐỘNG: Thêm hàm để khởi chạy và xử lý các lần nhấn nút.

Hàm đầu tiên khởi chạy bảng khi nhấn nút và hàm thứ hai gửi thông báo UDP nhiều hướng khi nhấn Nút 1.

/**
 * @brief Function to initialize the button.
 */
void otSysButtonInit(otSysButtonCallback aCallback)
{
    nrfx_gpiote_in_config_t in_config = NRFX_GPIOTE_CONFIG_IN_SENSE_LOTOHI(true);
    in_config.pull                    = NRF_GPIO_PIN_PULLUP;

    ret_code_t err_code;
    err_code = nrfx_gpiote_in_init(BUTTON_PIN, &in_config, in_pin1_handler);
    APP_ERROR_CHECK(err_code);

    sButtonHandler = aCallback;
    sButtonPressed = false;

    nrfx_gpiote_in_event_enable(BUTTON_PIN, true);
}

void otSysButtonProcess(otInstance *aInstance)
{
    if (sButtonPressed)
    {
        sButtonPressed = false;
        sButtonHandler(aInstance);
    }
}

HÀNH ĐỘNG: Lưu và đóng tệp gpio.c .

6. API: Phản ứng trước các thay đổi về vai trò trên thiết bị

Trong ứng dụng này, chúng ta muốn các đèn LED khác nhau sáng tuỳ theo vai trò của thiết bị. Hãy theo dõi các vai trò sau: Trưởng nhóm, Bộ định tuyến, Thiết bị cuối. Chúng ta có thể gán chúng cho đèn LED như sau:

  • LED1 = Thủ lĩnh
  • LED2 = Bộ định tuyến
  • LED3 = Thiết bị cuối

Để bật chức năng này, ứng dụng cần biết khi nào vai trò của thiết bị đã thay đổi và cách bật đúng đèn LED để phản hồi. Chúng ta sẽ sử dụng phiên bản OpenThread cho phần đầu tiên và bản tóm tắt nền tảng GPIO cho phần thứ hai.

Mở tệp ./openthread/examples/apps/cli/main.c trong trình chỉnh sửa văn bản bạn muốn dùng.

./openthread/examples/apps/cli/main.c

HÀNH ĐỘNG: Thêm tiêu đề bao gồm.

Trong phần "bao gồm" của tệp main.c, hãy thêm các tệp tiêu đề API mà bạn cần cho tính năng thay đổi vai trò.

#include <openthread/instance.h>
#include <openthread/thread.h>
#include <openthread/thread_ftd.h>

HÀNH ĐỘNG: Thêm nội dung khai báo hàm trình xử lý để thay đổi trạng thái của thực thể OpenThread.

Thêm phần khai báo này vào main.c, sau tiêu đề bao gồm và trước mọi câu lệnh #if. Hàm này sẽ được định nghĩa sau ứng dụng chính.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

HÀNH ĐỘNG: Thêm lệnh đăng ký lệnh gọi lại cho hàm của trình xử lý thay đổi trạng thái.

Trong main.c, hãy thêm hàm này vào hàm main() sau lệnh gọi otAppCliInit. Việc đăng ký lệnh gọi lại này sẽ yêu cầu OpenThread gọi hàm handleNetifStateChange bất cứ khi nào trạng thái của thực thể OpenThread thay đổi.

/* Register Thread state change handler */
otSetStateChangedCallback(instance, handleNetifStateChanged, instance);

HÀNH ĐỘNG: Thêm phương thức triển khai thay đổi trạng thái.

Trong main.c, sau hàm main(), hãy triển khai hàm handleNetifStateChanged. Hàm này kiểm tra cờ OT_CHANGED_THREAD_ROLE của thực thể OpenThread và nếu cờ này có thay đổi, hãy bật/tắt đèn LED khi cần.

void handleNetifStateChanged(uint32_t aFlags, void *aContext)
{
   if ((aFlags & OT_CHANGED_THREAD_ROLE) != 0)
   {
       otDeviceRole changedRole = otThreadGetDeviceRole(aContext);

       switch (changedRole)
       {
       case OT_DEVICE_ROLE_LEADER:
           otSysLedSet(1, true);
           otSysLedSet(2, false);
           otSysLedSet(3, false);
           break;

       case OT_DEVICE_ROLE_ROUTER:
           otSysLedSet(1, false);
           otSysLedSet(2, true);
           otSysLedSet(3, false);
           break;

       case OT_DEVICE_ROLE_CHILD:
           otSysLedSet(1, false);
           otSysLedSet(2, false);
           otSysLedSet(3, true);
           break;

       case OT_DEVICE_ROLE_DETACHED:
       case OT_DEVICE_ROLE_DISABLED:
           /* Clear LED4 if Thread is not enabled. */
           otSysLedSet(4, false);
           break;
        }
    }
}

7. API: Dùng chế độ phát đa hướng để bật đèn LED

Trong ứng dụng này, chúng ta cũng muốn gửi thông báo UDP đến tất cả thiết bị khác trong mạng khi nhấn Nút 1 trên một bảng. Để xác nhận đã nhận được tin nhắn, chúng ta sẽ bật/tắt LED4 trên các bảng khác.

Để bật chức năng này, ứng dụng cần:

  • Khởi động kết nối UDP khi khởi động
  • Có thể gửi thông báo UDP đến địa chỉ truyền đa hướng cục bộ trên lưới
  • Xử lý thông báo UDP đến
  • Bật/tắt LED4 để phản hồi tin nhắn UDP đến

Mở tệp ./openthread/examples/apps/cli/main.c trong trình chỉnh sửa văn bản bạn muốn dùng.

./openthread/examples/apps/cli/main.c

HÀNH ĐỘNG: Thêm tiêu đề bao gồm.

Trong phần include ở đầu tệp main.c, hãy thêm các tệp tiêu đề API mà bạn cần cho tính năng UDP nhiều hướng.

#include <string.h>

#include <openthread/message.h>
#include <openthread/udp.h>

#include "utils/code_utils.h"

Tiêu đề code_utils.h được dùng cho macro otEXPECTotEXPECT_ACTION để xác thực điều kiện thời gian chạy và xử lý lỗi một cách linh hoạt.

ACTION: Thêm định nghĩa và hằng số:

Trong tệp main.c, sau phần include và trước mọi câu lệnh #if, hãy thêm các hằng số dành riêng cho UDP và định nghĩa:

#define UDP_PORT 1212

static const char UDP_DEST_ADDR[] = "ff03::1";
static const char UDP_PAYLOAD[]   = "Hello OpenThread World!";

ff03::1 là địa chỉ truyền đa hướng cục bộ trên lưới. Mọi thư gửi đến địa chỉ này sẽ được gửi đến tất cả Thiết bị toàn chuỗi trong mạng. Vui lòng xem phần Multicast trên openthread.io để biết thêm thông tin về tính năng hỗ trợ phát đa hướng trong OpenThread.

HÀNH ĐỘNG: Thêm nội dung khai báo hàm.

Trong tệp main.c, sau định nghĩa otTaskletsSignalPending và trước hàm main(), hãy thêm các hàm dành riêng cho UDP cũng như một biến tĩnh để đại diện cho cổng UDP:

static void initUdp(otInstance *aInstance);
static void sendUdp(otInstance *aInstance);

static void handleButtonInterrupt(otInstance *aInstance);

void handleUdpReceive(void *aContext, otMessage *aMessage, 
                      const otMessageInfo *aMessageInfo);

static otUdpSocket sUdpSocket;

HÀNH ĐỘNG: Thêm lệnh gọi để khởi chạy nút và đèn LED GPIO.

Trong main.c, hãy thêm các lệnh gọi hàm này vào hàm main() sau lệnh gọi otSetStateChangedCallback. Các hàm này khởi động các chân GPIO và GPIOTE, đồng thời thiết lập một trình xử lý nút để xử lý các sự kiện đẩy nút.

/* init GPIO LEDs and button */
otSysLedInit();
otSysButtonInit(handleButtonInterrupt);

HÀNH ĐỘNG: Thêm lệnh gọi khởi chạy UDP.

Trong main.c, hãy thêm hàm này vào hàm main() sau lệnh gọi otSysButtonInit mà bạn vừa thêm:

initUdp(instance);

Lệnh gọi này đảm bảo cổng UDP được khởi chạy khi khởi động ứng dụng. Nếu không, thiết bị sẽ không thể gửi hoặc nhận thông báo UDP.

HÀNH ĐỘNG: Thêm lệnh gọi để xử lý sự kiện nút GPIO.

Trong main.c, hãy thêm lệnh gọi hàm này vào hàm main() sau lệnh gọi otSysProcessDrivers, trong vòng lặp while. Hàm này được khai báo trong gpio.c, kiểm tra xem người dùng có nhấn vào nút này hay không. Nếu có, sẽ gọi trình xử lý (handleButtonInterrupt) đã được thiết lập ở bước trên.

otSysButtonProcess(instance);

HÀNH ĐỘNG: Triển khai Trình xử lý gián đoạn nút.

Trong main.c, hãy thêm phương thức triển khai hàm handleButtonInterrupt sau hàm handleNetifStateChanged mà bạn đã thêm ở bước trước:

/**
 * Function to handle button push event
 */
void handleButtonInterrupt(otInstance *aInstance)
{
    sendUdp(aInstance);
}

HÀNH ĐỘNG: Triển khai quy trình khởi chạy UDP.

Trong main.c, hãy thêm phương thức triển khai hàm initUdp sau hàm handleButtonInterrupt mà bạn vừa thêm:

/**
 * Initialize UDP socket
 */
void initUdp(otInstance *aInstance)
{
    otSockAddr  listenSockAddr;

    memset(&sUdpSocket, 0, sizeof(sUdpSocket));
    memset(&listenSockAddr, 0, sizeof(listenSockAddr));

    listenSockAddr.mPort    = UDP_PORT;

    otUdpOpen(aInstance, &sUdpSocket, handleUdpReceive, aInstance);
    otUdpBind(aInstance, &sUdpSocket, &listenSockAddr, OT_NETIF_THREAD);
}

UDP_PORT là cổng mà bạn đã xác định trước đó (1212). Hàm otUdpOpen mở cổng và đăng ký hàm callback (handleUdpReceive) khi nhận được thông báo UDP. otUdpBind liên kết ổ cắm với giao diện mạng Thread bằng cách truyền OT_NETIF_THREAD. Để biết các tuỳ chọn giao diện mạng khác, hãy tham khảo bảng liệt kê otNetifIdentifier trong Tài liệu tham khảo API UDP.

HÀNH ĐỘNG: Triển khai thông báo UDP

Trong main.c, hãy thêm phương thức triển khai hàm sendUdp sau hàm initUdp mà bạn vừa thêm:

/**
 * Send a UDP datagram
 */
void sendUdp(otInstance *aInstance)
{
    otError       error = OT_ERROR_NONE;
    otMessage *   message;
    otMessageInfo messageInfo;
    otIp6Address  destinationAddr;

    memset(&messageInfo, 0, sizeof(messageInfo));

    otIp6AddressFromString(UDP_DEST_ADDR, &destinationAddr);
    messageInfo.mPeerAddr    = destinationAddr;
    messageInfo.mPeerPort    = UDP_PORT;

    message = otUdpNewMessage(aInstance, NULL);
    otEXPECT_ACTION(message != NULL, error = OT_ERROR_NO_BUFS);

    error = otMessageAppend(message, UDP_PAYLOAD, sizeof(UDP_PAYLOAD));
    otEXPECT(error == OT_ERROR_NONE);

    error = otUdpSend(aInstance, &sUdpSocket, message, &messageInfo);

 exit:
    if (error != OT_ERROR_NONE && message != NULL)
    {
        otMessageFree(message);
    }
}

Hãy lưu ý macro otEXPECTotEXPECT_ACTION. Các điều này đảm bảo thông báo UDP là hợp lệ và được phân bổ chính xác trong vùng đệm. Nếu không, hàm này sẽ xử lý lỗi một cách linh hoạt bằng cách chuyển đến khối exit để giải phóng bộ đệm.

Vui lòng xem Tài liệu tham khảo IPv6UDP trên openthread.io để biết thêm thông tin về các hàm dùng để khởi chạy UDP.

HÀNH ĐỘNG: Triển khai cách xử lý thông báo UDP.

Trong main.c, hãy thêm phương thức triển khai hàm handleUdpReceive sau hàm sendUdp mà bạn vừa thêm. Chức năng này chỉ bật/tắt LED4.

/**
 * Function to handle UDP datagrams received on the listening socket
 */
void handleUdpReceive(void *aContext, otMessage *aMessage,
                      const otMessageInfo *aMessageInfo)
{
    OT_UNUSED_VARIABLE(aContext);
    OT_UNUSED_VARIABLE(aMessage);
    OT_UNUSED_VARIABLE(aMessageInfo);

    otSysLedToggle(4);
}

8. API: Định cấu hình mạng Thread

Để dễ minh hoạ, chúng ta muốn các thiết bị của mình khởi động Thread ngay lập tức và kết hợp với nhau thành một mạng khi bật nguồn. Để làm điều này, chúng ta sẽ sử dụng cấu trúc otOperationalDataset. Cấu trúc này chứa tất cả các tham số cần thiết để truyền thông tin xác thực mạng Thread đến một thiết bị.

Việc sử dụng cấu trúc này sẽ ghi đè các giá trị mặc định mạng được tích hợp vào OpenThread, giúp ứng dụng an toàn hơn và giới hạn các nút Thread trong mạng thành chỉ những nút đang chạy ứng dụng.

Một lần nữa, mở tệp ./openthread/examples/apps/cli/main.c trong trình chỉnh sửa văn bản bạn muốn dùng.

./openthread/examples/apps/cli/main.c

HÀNH ĐỘNG: Thêm tiêu đề include.

Trong phần include ở đầu tệp main.c, hãy thêm tệp tiêu đề API mà bạn cần để định cấu hình mạng Thread:

#include <openthread/dataset_ftd.h>

HÀNH ĐỘNG: Thêm nội dung khai báo hàm để thiết lập cấu hình mạng.

Thêm phần khai báo này vào main.c, sau tiêu đề bao gồm và trước mọi câu lệnh #if. Hàm này sẽ được định nghĩa sau hàm ứng dụng chính.

static void setNetworkConfiguration(otInstance *aInstance);

HÀNH ĐỘNG: Thêm lệnh gọi cấu hình mạng.

Trong main.c, hãy thêm lệnh gọi hàm này vào hàm main() sau lệnh gọi otSetStateChangedCallback. Hàm này định cấu hình tập dữ liệu mạng Thread.

/* Override default network credentials */
setNetworkConfiguration(instance);

HÀNH ĐỘNG: Thêm lệnh gọi để kích hoạt ngăn xếp và giao diện mạng Thread

Trong main.c, hãy thêm các lệnh gọi hàm này vào hàm main() sau lệnh gọi otSysButtonInit.

/* Start the Thread network interface (CLI cmd > ifconfig up) */
otIp6SetEnabled(instance, true);

/* Start the Thread stack (CLI cmd > thread start) */
otThreadSetEnabled(instance, true);

HÀNH ĐỘNG: Triển khai cấu hình mạng Thread

Trong main.c, hãy thêm phương thức triển khai hàm setNetworkConfiguration sau hàm main():

/**
 * Override default network settings, such as panid, so the devices can join a
 network
 */
void setNetworkConfiguration(otInstance *aInstance)
{
    static char          aNetworkName[] = "OTCodelab";
    otOperationalDataset aDataset;

    memset(&aDataset, 0, sizeof(otOperationalDataset));

    /*
     * Fields that can be configured in otOperationDataset to override defaults:
     *     Network Name, Mesh Local Prefix, Extended PAN ID, PAN ID, Delay Timer,
     *     Channel, Channel Mask Page 0, Network Key, PSKc, Security Policy
     */
    aDataset.mActiveTimestamp.mSeconds             = 1;
    aDataset.mActiveTimestamp.mTicks               = 0;
    aDataset.mActiveTimestamp.mAuthoritative       = false;
    aDataset.mComponents.mIsActiveTimestampPresent = true;

    /* Set Channel to 15 */
    aDataset.mChannel                      = 15;
    aDataset.mComponents.mIsChannelPresent = true;

    /* Set Pan ID to 2222 */
    aDataset.mPanId                      = (otPanId)0x2222;
    aDataset.mComponents.mIsPanIdPresent = true;

    /* Set Extended Pan ID to C0DE1AB5C0DE1AB5 */
    uint8_t extPanId[OT_EXT_PAN_ID_SIZE] = {0xC0, 0xDE, 0x1A, 0xB5, 0xC0, 0xDE, 0x1A, 0xB5};
    memcpy(aDataset.mExtendedPanId.m8, extPanId, sizeof(aDataset.mExtendedPanId));
    aDataset.mComponents.mIsExtendedPanIdPresent = true;

    /* Set network key to 1234C0DE1AB51234C0DE1AB51234C0DE */
    uint8_t key[OT_NETWORK_KEY_SIZE] = {0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE};
    memcpy(aDataset.mNetworkKey.m8, key, sizeof(aDataset.mNetworkKey));
    aDataset.mComponents.mIsNetworkKeyPresent = true;

    /* Set Network Name to OTCodelab */
    size_t length = strlen(aNetworkName);
    assert(length <= OT_NETWORK_NAME_MAX_SIZE);
    memcpy(aDataset.mNetworkName.m8, aNetworkName, length);
    aDataset.mComponents.mIsNetworkNamePresent = true;

    otDatasetSetActive(aInstance, &aDataset);
    /* Set the router selection jitter to override the 2 minute default.
       CLI cmd > routerselectionjitter 20
       Warning: For demo purposes only - not to be used in a real product */
    uint8_t jitterValue = 20;
    otThreadSetRouterSelectionJitter(aInstance, jitterValue);
}

Như đã trình bày chi tiết trong hàm này, các tham số mạng Thread mà chúng ta đang sử dụng cho ứng dụng này là:

  • Kênh = 15
  • Mã số PAN = 0x2222
  • Mã PAN mở rộng = C0DE1AB5C0DE1AB5
  • Chìa khoá mạng = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Tên mạng = OTCodelab

Ngoài ra, đây là nơi chúng ta giảm Dao động lựa chọn bộ định tuyến, vì vậy thiết bị thay đổi vai trò nhanh hơn cho mục đích minh hoạ. Lưu ý rằng thao tác này chỉ được thực hiện nếu nút đó là FTD (Thiết bị luồng đầy đủ). Tìm hiểu thêm về điều đó trong bước tiếp theo.

9. API: Các hàm bị hạn chế

Một số API của OpenThread sửa đổi các chế độ cài đặt chỉ nên sửa đổi cho mục đích minh hoạ hoặc kiểm thử. Bạn không nên dùng các API này trong quá trình triển khai chính thức một ứng dụng bằng OpenThread.

Ví dụ: hàm otThreadSetRouterSelectionJitter điều chỉnh thời gian (tính bằng giây) mà Thiết bị cuối cần để tự quảng bá với Bộ định tuyến. Giá trị mặc định cho giá trị này là 120, theo Thông số kỹ thuật luồng. Để dễ sử dụng trong lớp học lập trình này, chúng ta sẽ đổi thành 20, vì vậy, bạn không phải đợi quá lâu để một nút Thread thay đổi vai trò.

Lưu ý: Thiết bị MTD không trở thành bộ định tuyến và bản dựng MTD không hỗ trợ một hàm như otThreadSetRouterSelectionJitter. Sau này, chúng ta cần chỉ định tuỳ chọn CMake -DOT_MTD=OFF, nếu không, chúng ta sẽ gặp lỗi bản dựng.

Bạn có thể xác nhận điều này bằng cách xem định nghĩa hàm otThreadSetRouterSelectionJitter nằm trong lệnh của bộ tiền xử lý OPENTHREAD_FTD:

./openthread/src/core/api/thread_ftd_api.cpp

#if OPENTHREAD_FTD

#include <openthread/thread_ftd.h>

...

void otThreadSetRouterSelectionJitter(otInstance *aInstance, uint8_t aRouterJitter)
{
    Instance &instance = *static_cast<Instance *>(aInstance);

    instance.GetThreadNetif().GetMle().SetRouterSelectionJitter(aRouterJitter);
}

...

#endif // OPENTHREAD_FTD

10. Cập nhật CMake

Trước khi xây dựng ứng dụng, bạn cần thực hiện một số cập nhật nhỏ cho 3 tệp CMake. Hệ thống xây dựng sử dụng các tệp này để biên dịch và liên kết ứng dụng của bạn.

./third_party/NordicSemiconductor/CMakeLists.txt

Bây giờ, hãy thêm một số cờ vào CMakeLists.txt của NordicSemiconductor để đảm bảo các hàm GPIO được xác định trong ứng dụng.

HÀNH ĐỘNG: Thêm cờ vào tệp CMakeLists.txt .

Mở ./third_party/NordicSemiconductor/CMakeLists.txt trong trình chỉnh sửa văn bản mà bạn muốn và thêm các dòng sau vào phần COMMON_FLAG.

...
set(COMMON_FLAG
    -DSPIS_ENABLED=1
    -DSPIS0_ENABLED=1
    -DNRFX_SPIS_ENABLED=1
    -DNRFX_SPIS0_ENABLED=1
    ...

    # Defined in ./third_party/NordicSemiconductor/nrfx/templates/nRF52840/nrfx_config.h
    -DGPIOTE_ENABLED=1
    -DGPIOTE_CONFIG_IRQ_PRIORITY=7
    -DGPIOTE_CONFIG_NUM_OF_LOW_POWER_EVENTS=1
)

...

./src/CMakeLists.txt

Chỉnh sửa tệp ./src/CMakeLists.txt để thêm tệp nguồn gpio.c mới:

HÀNH ĐỘNG: Thêm nguồn gpio vào tệp ./src/CMakeLists.txt .

Mở ./src/CMakeLists.txt trong trình chỉnh sửa văn bản mà bạn muốn sử dụng rồi thêm tệp này vào phần NRF_COMM_SOURCES.

...

set(NRF_COMM_SOURCES
  ...
  src/gpio.c
  ...
)

...

./third_party/NordicSemiconductor/CMakeLists.txt

Cuối cùng, hãy thêm tệp trình điều khiển nrfx_gpiote.c vào tệp CMakeLists.txt của NordicSemiconductor để tệp này có trong bản dựng thư viện của trình điều khiển Bắc Âu.

HÀNH ĐỘNG: Thêm trình điều khiển gpio vào tệp CMakeLists.txt của NordicSemiconductor.

Mở ./third_party/NordicSemiconductor/CMakeLists.txt trong trình chỉnh sửa văn bản mà bạn muốn sử dụng rồi thêm tệp này vào phần COMMON_SOURCES.

...

set(COMMON_SOURCES
  ...
  nrfx/drivers/src/nrfx_gpiote.c
  ...
)
...

11. Thiết lập thiết bị

Với tất cả các bản cập nhật mã đã hoàn tất, bạn đã sẵn sàng xây dựng và cài đặt ứng dụng cho cả ba bảng phát triển nRF52840 của Bắc Âu. Mỗi thiết bị sẽ hoạt động như một Thiết bị luồng đầy đủ (FTD).

Tạo OpenThread

Xây dựng các tệp nhị phân OpenThread FTD cho nền tảng nRF52840.

$ cd ~/ot-nrf528xx
$ ./script/build nrf52840 UART_trans -DOT_MTD=OFF -DOT_APP_RCP=OFF -DOT_RCP=OFF

Chuyển đến thư mục có tệp nhị phân OpenThread FTD CLI và chuyển đổi tệp đó sang định dạng hex bằng ARM Embedded Toolchain:

$ cd build/bin
$ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex

Bật nhanh

Cài đặt ROM tệp ot-cli-ftd.hex vào từng bảng nRF52840.

Gắn cáp USB vào cổng gỡ lỗi Micro-USB ở bên cạnh chân nguồn bên ngoài trên bảng nRF52840, sau đó cắm vào máy Linux. Nếu bạn đặt đúng đèn, LED5 sẽ bật.

20a3b4b480356447.pngS

Như trước đó, hãy lưu ý số sê-ri của bảng nRF52840:

c00d519ebec7e5f0.jpeg

Điều hướng đến vị trí của Công cụ dòng lệnh nRFx và cài đặt tệp hex OpenThread CLI FTD lên bảng nRF52840, sử dụng số sê-ri của bảng:

$ cd ~/nrfjprog
$ ./nrfjprog -f nrf52 -s 683704924 --verify --chiperase --program \
       ~/openthread/output/nrf52840/bin/ot-cli-ftd.hex --reset

Đèn LED5 sẽ tắt một lát trong khi nhấp nháy. Kết quả sau đây được tạo khi thành công:

Parsing hex file.
Erasing user available code and UICR flash areas.
Applying system reset.
Checking that the area to write is not protected.
Programing device.
Applying system reset.
Run.

Lặp lại thao tác "Cài đặt nhanh bảng" này cho hai bảng còn lại. Mỗi bảng phải được kết nối với máy Linux theo cùng cách và lệnh nhấp nháy là giống nhau, ngoại trừ số sê-ri của bảng. Nhớ sử dụng số sê-ri duy nhất của mỗi bảng trong

nrfjprog lệnh nhấp nháy.

Nếu thành công, LED1, LED2 hoặc LED3 sẽ sáng trên mỗi bảng. Thậm chí bạn có thể nhìn thấy công tắc đèn LED sáng từ 3 sang 2 (hoặc 2 sang 1) ngay sau khi nhấp nháy (tính năng thay đổi vai trò thiết bị).

12. Chức năng của ứng dụng

Cả ba bảng nRF52840 hiện đã được cấp nguồn và chạy ứng dụng OpenThread của chúng tôi. Như đã trình bày chi tiết trước đó, ứng dụng này có hai tính năng chính.

Chỉ báo vai trò của thiết bị

Đèn LED sáng trên mỗi bảng phản ánh vai trò hiện tại của nút Thread:

  • LED1 = Thủ lĩnh
  • LED2 = Bộ định tuyến
  • LED3 = Thiết bị cuối

Khi vai trò thay đổi, đèn LED cũng sẽ thay đổi. Chắc hẳn bạn đã thấy những thay đổi này trên một hoặc hai bảng trong vòng 20 giây kể từ khi mỗi thiết bị bật nguồn.

Đa hướng UDP

Khi người dùng nhấn Nút 1 trên một bảng, thông báo UDP sẽ được gửi đến địa chỉ truyền đa hướng cục bộ trên lưới. Địa chỉ này bao gồm tất cả các nút khác trong mạng Thread. Để nhận được thông báo này, LED4 trên tất cả các bảng khác sẽ bật hoặc tắt. LED4 vẫn bật hoặc tắt cho từng bảng cho đến khi nhận được thông báo UDP khác.

203dd094acca1f97.pngS

9bbd96d9b1c63504.pngS

13. Bản minh hoạ: Quan sát các thay đổi đối với vai trò trên thiết bị

Những thiết bị bạn đã cài đặt ROM là một loại Thiết bị chuỗi đầy đủ (FTD) cụ thể được gọi là Thiết bị đầu đủ điều kiện cho bộ định tuyến (REED). Điều này có nghĩa là các thiết bị này có thể hoạt động như một Bộ định tuyến hoặc Thiết bị cuối và có thể tự quảng bá từ một Thiết bị cuối đến Bộ định tuyến.

Thread có thể hỗ trợ tối đa 32 Bộ định tuyến, nhưng cố gắng giữ số lượng Bộ định tuyến trong khoảng từ 16 đến 23. Nếu một REED đính kèm dưới dạng một Thiết bị cuối và số lượng Bộ định tuyến nhỏ hơn 16, thì nó sẽ tự động quảng bá lên một Bộ định tuyến. Thay đổi này sẽ xảy ra vào một thời điểm ngẫu nhiên trong phạm vi số giây mà bạn đặt giá trị otThreadSetRouterSelectionJitter trong ứng dụng (20 giây).

Mỗi mạng Thread đều có một Thủ lĩnh. Đó là một Bộ định tuyến chịu trách nhiệm quản lý tập hợp các Bộ định tuyến trong mạng Thread. Khi tất cả các thiết bị đang bật, sau 20 giây, một trong số các thiết bị đó sẽ là Thiết bị dẫn đầu (LED1 bật) và hai thiết bị còn lại sẽ là Bộ định tuyến (LED2 bật).

4e1e885861a66570.pngS

Xoá Người lãnh đạo

Nếu Trưởng nhóm bị xoá khỏi mạng Thread, một Bộ định tuyến khác sẽ tự thăng cấp nó thành Trưởng nhóm, để đảm bảo mạng vẫn còn một Trưởng nhóm.

Tắt Bảng điều khiển lãnh đạo (bảng có đèn LED1 sáng) bằng công tắc Nguồn. Đợi khoảng 20 giây. Trên một trong hai bo mạch còn lại, LED2 (Bộ định tuyến) sẽ tắt và đèn LED1 (Leader) sẽ bật. Thiết bị này hiện là thiết bị lãnh đạo của mạng Thread.

4c57c87adb40e0e3.png.

Bật lại bảng lãnh đạo ban đầu. Thiết bị sẽ tự động kết nối lại với mạng Thread với tư cách là một Thiết bị cuối (LED3 sáng). Trong vòng 20 giây (Biến động lựa chọn bộ định tuyến) nó sẽ tự quảng bá lên Bộ định tuyến (LED2 sáng).

5f40fca2dcc4b5b.png.

Đặt lại các bảng

Tắt cả 3 bảng, sau đó bật lại và quan sát các đèn LED. Bảng đầu tiên được cấp nguồn sẽ bắt đầu trong vai trò Người lãnh đạo (đèn LED1 sáng) – Bộ định tuyến đầu tiên trong mạng Thread sẽ tự động trở thành Người lãnh đạo.

Hai bảng còn lại ban đầu kết nối với mạng dưới dạng Thiết bị cuối (LED3 sáng) nhưng sẽ tự quảng bá cho Bộ định tuyến (LED2 sáng) trong vòng 20 giây.

Phân vùng mạng

Nếu các bo mạch của bạn không nhận đủ điện năng hoặc kết nối vô tuyến giữa chúng yếu, mạng Thread có thể chia thành các phân vùng và bạn có thể có nhiều thiết bị hiển thị là thiết bị Leader.

Luồng đang tự khôi phục, vì vậy, các phân vùng sẽ dần hợp nhất trở lại thành một phân vùng duy nhất với một Trưởng nhóm.

14. Bản minh hoạ: Gửi đa hướng UDP

Nếu bạn tiếp tục bài tập trước, đèn LED4 sẽ không sáng trên bất kỳ thiết bị nào.

Chọn một bảng bất kỳ rồi nhấn Nút 1. LED4 trên tất cả các bảng khác trong mạng Thread đang chạy ứng dụng sẽ bật/tắt trạng thái của chúng. Nếu bạn tiếp tục bài tập trước thì giờ đây, các mã đó sẽ bắt đầu chạy.

f186a2618fdbe3fd.png

Nhấn lại Nút 1 để thoát khỏi bảng tương tự. Đèn LED4 trên tất cả các bảng khác sẽ bật/tắt lại.

Nhấn Nút 1 trên một bảng khác và quan sát cách LED4 bật/tắt trên các bảng khác. Nhấn Nút 1 trên một trong những bảng mạch đang bật đèn LED4. Đèn LED4 vẫn bật trên bảng đó nhưng bật trên các bảng khác.

f5865ccb8ab7aa34.png

Phân vùng mạng

Nếu các bảng của bạn đã được phân vùng và có nhiều Người lãnh đạo trong số đó, thì kết quả của thông báo phát đa hướng sẽ khác nhau giữa các bảng. Nếu bạn nhấn Button1 trên một bảng đã được phân vùng (và do đó là thành viên duy nhất của mạng Thread được phân vùng), LED4 trên các bảng khác sẽ không sáng lên để phản hồi. Nếu điều này xảy ra, hãy đặt lại các bảng – lý tưởng là các bảng này sẽ cải cách một mạng Thread và thông báo UDP sẽ hoạt động chính xác.

15. Xin chúc mừng!

Bạn đã tạo một ứng dụng sử dụng API OpenThread!

Giờ đây, bạn đã biết:

  • Cách lập trình các nút và đèn LED trên bảng nhà phát triển Bắc Âu nRF52840
  • Cách sử dụng các API OpenThread phổ biến và lớp otInstance
  • Cách theo dõi và phản ứng với các thay đổi về trạng thái OpenThread
  • Cách gửi thông báo UDP đến tất cả thiết bị trong mạng Thread
  • Cách sửa đổi Makefile

Các bước tiếp theo

Dựa trên lớp học lập trình này, hãy thử thực hiện các bài tập sau:

  • Sửa đổi mô-đun GPIO để sử dụng chân GPIO thay vì đèn LED tích hợp, đồng thời kết nối các đèn LED RGB bên ngoài có thể thay đổi màu dựa trên vai trò của Bộ định tuyến
  • Thêm tính năng hỗ trợ GPIO cho một nền tảng mẫu khác
  • Thay vì sử dụng chế độ phát đa hướng để ping tất cả thiết bị khi nhấn nút, hãy sử dụng API Bộ định tuyến/lãnh đạo để định vị và ping một thiết bị riêng lẻ
  • Kết nối mạng lưới của bạn với Internet bằng cách sử dụng Bộ định tuyến đường viền OpenThread và phát đa hướng chúng từ bên ngoài mạng Thread để chiếu sáng đèn LED

Tài liệu đọc thêm

Hãy tham khảo openthread.ioGitHub để xem nhiều tài nguyên OpenThread, bao gồm:

Tham khảo: