Phát triển bằng API OpenThread

1. Giới thiệu

26b7f4f6b3ea0700.png.

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 đã phát hành OpenThread để cung cấp rộng rãi công nghệ cho các sản phẩm Nest cho các nhà phát triển nhằm đẩy nhanh quá trình phát triển sản phẩm cho ngôi nhà thông minh.

Đặc tả luồng xác định một giao thức truyền dữ liệu từ thiết bị tới thiết bị không dây công suất thấp, bảo mật và đáng tin cậy dựa trên IPv6 cho các ứng dụng gia đình. OpenThread triển khai 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, Thiết 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 các API OpenThread để bắt đầu một mạng Thread, theo dõi và phản ứng với các thay đổi về vai trò của thiết bị, gửi thông báo UDP, cũng như 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 tế.

2a6db2e258c32237.png.

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 bo mạch phát triển Nordic 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 của OpenThread
  • Cách gửi tin nhắn UDP đến tất cả các thiết bị trong mạng chuỗi
  • Cách sửa đổi tệp Makefile

Bạn cần có

Phần cứng:

  • 3 bo mạch nhà phát triển Bắc Âu Semiconductor nRF52840
  • 3 cáp USB với Micro-USB để kết nối bo mạch
  • 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

Trừ khi có ghi chú 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 thành lớp học lập trình Xây dựng mạng luồng với bảng nRF52840 và OpenThread:

  • Chi tiết tất cả phần mềm bạn cần để xây dựng và cài đặt ROM
  • Hướng dẫn bạn cách tạo OpenThread và cài đặt FlashThread trên các bo mạch Bắc Âu nRF52840
  • Minh họa các khái niệm cơ bản về Mạng luồng

Không có môi trường nào được thiết lập để tạo OpenThread và flash bảng được nêu chi tiết trong Lớp học lập trình này - chỉ có hướng dẫn cơ bản để cài đặt flash bảng. Giả sử bạn đã hoàn thành lớp học lập trình về Xây dựng mạng lưới luồ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 i386 hoặc x86 để flash tất cả các bảng phát triển Thread. Tất cả các bước đã được thử nghiệm trên Ubuntu 14.04.5 LTS (Trusty Tahr).

Bo mạch bán dẫn Bắc Âu nRF52840

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

a6693da3ce213856.png.

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

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

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

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

Sao chép các mẫu 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ề OpenThread API

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 cho phép 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:

  • Kiểm soát và thông tin phiên bản OpenThread
  • Các dịch vụ ứng dụng như IPv6, UDP và CoAP
  • Quản lý thông tin xác thực 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

Bạn có thể xem thông tin tham khảo về tất cả các API OpenThread tại openthread.io/reference.

Sử dụng API

Để sử dụng API, hãy bao gồm tệp tiêu đề của API vào một trong các tệp ứng dụng của bạn. Sau đó, 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>

Phiên bản OpenThread

Cấu trúc otInstance là cấu hình mà bạn thường xuyên sử 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 phiên bản tĩnh của thư viện OpenThread và cho phép người dùng gọi API OpenThread.

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 đi kèm với OpenThread, trước tiên, hãy khai báo các hàm này 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 chúng trong một tệp nguồn dành riêng cho nền tảng. 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ụ: các hàm GPIO mà chúng ta sẽ sử dụng để kết nối với các nút nRF52840 và đèn LED phải được khai báo trong openthread-system.h.

Mở tệp ./openthread/examples/platforms/openthread-system.h trong trình soạn thảo văn bản yêu thích của bạn.

./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 khai báo hàm 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 tôi sẽ triển khai các tính năng này trong bước tiếp theo.

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

5. Triển khai tính năng trừu tượng hóa nền tảng GPIO

Trong bước trước, chúng ta đã tìm hiểu các phần 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 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 chân và chế độ GPIO
  • Điều khiển điện áp trên chân
  • Bật 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 là gpio.c. Trong tệp mới này, hãy thêm nội dung sau.

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

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

Các định nghĩa này đóng vai trò là bản tóm tắt giữa các giá trị cụ thể nRF52840 và các biến được 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 nRF52840, hãy xem Trung tâm thông tin 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 cần thiết 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à chức năng 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 chức năng nhấn nút được khởi chạy (sau này trong tệp này).

Xin lưu ý rằng cách 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 tạo.

/**
 * @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 một hàm để bật/tắt chế độ của đèn LED.

Hàm này sẽ được dùng để bật/tắt đèn LED4 khi thiết bị nhận được thông báo UDP phát đa 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 tạo và xử lý các thao tác nhấn nút.

Hàm đầu tiên khởi chạy board cho một lần nhấn nút, và hàm thứ hai gửi thông báo UDP phát đa hướng khi Nút 1 được nhấn.

/**
 * @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 với các thay đổi về vai trò của thiết bị

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

  • LED1 = Lãnh đạo
  • 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 thời điểm 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à phần trừu tượng cho nền tảng GPIO cho phần thứ hai.

Mở tệp ./openthread/examples/apps/cli/main.c trong trình soạn thảo văn bản yêu thích của bạn.

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

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

Trong phần include 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 phần khai báo hàm xử lý cho thay đổi trạng thái của OpenThread.

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

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

HÀNH ĐỘNG: Thêm gói đăng ký lệnh gọi lại cho hàm 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 yêu cầu OpenThread gọi hàm handleNetifStateChange bất cứ khi nào trạng thái thực thể OpenThread thay đổi.

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

HÀNH ĐỘNG: Thêm nội dung 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 nó đã 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: Sử dụng phát đa hướng để bật đèn LED

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

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

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

Mở tệp ./openthread/examples/apps/cli/main.c trong trình soạn thảo văn bản yêu thích của bạn.

./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 phát đa 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 các macro otEXPECTotEXPECT_ACTION để xác thực các điều kiện thời gian chạy và xử lý lỗi một cách linh hoạt.

HÀNH ĐỘNG: Thêm định nghĩa và hằng số:

Trong tệp main.c, sau phần include và trước bất kỳ câu lệnh #if nào, hãy thêm hằng số dành riêng cho UDP và xác định:

#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ỉ phát đa hướng cục bộ ở dạng lưới. Mọi thư được gửi đến địa chỉ này sẽ được gửi tới tất cả các Thiết bị Luồng Toàn bộ trong mạng. Vui lòng xem nội dung Đa hướng trên openthread.io để biết thêm thông tin về 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 cụ thể UDP, cũng như một biến tĩnh để đại diện cho một ổ cắm 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 động đèn LED và nút 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 tạo các chân GPIO và GPIOTE, đồng thời đặt một trình xử lý nút để xử lý các sự kiện nhấn 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ắm UDP được khởi động khi khởi động ứng dụng. Nếu không, thiết bị không thể gửi hoặc nhận tin nhắn 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 sẽ kiểm tra xem nút đã được nhấn hay chưa. Nếu có, hãy 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 cách 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 UDP.

Trong main.c, hãy thêm cách 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 bạn đã khai báo trước đó (1212). Hàm otUdpOpen mở ổ cắm và đăng ký hàm gọi lại (handleUdpReceive) khi nhận được thông báo UDP. otUdpBind liên kết ổ cắm với giao diện mạng Chuỗi 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 tính năng nhắn tin UDP.

Trong main.c, hãy thêm cách 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. Việc này đảm bảo rằng thông báo UDP là hợp lệ và được phân bổ chính xác trong bộ đệm. Nếu không, hàm sẽ xử lý lỗi một cách linh hoạt bằng cách chuyển đến khối exit, nơi bộ nhớ này giải phóng bộ đệm.

Hãy xem tài liệu tham khảo về 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 tính năng xử lý tin nhắn UDP.

Trong main.c, hãy thêm cách 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 đèn 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 luồng

Để dễ dàng minh họa, chúng tôi muốn các thiết bị của mình khởi động ngay lập tức Luồng và kết nối với nhau thành một mạng khi bật nguồn. Để thực hiện việc 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 của luồng Thread đến thiết bị.

Việc sử dụng cấu trúc này sẽ ghi đè các giá trị mặc định của mạng được tích hợp sẵn trong OpenThread, để giúp ứng dụng của chúng tôi bảo mật hơn và giới hạn các nút Thread trong mạng của chúng tôi chỉ là các nút chạy ứng dụng.

Mở lại tệp ./openthread/examples/apps/cli/main.c trong trình soạn thảo văn bản yêu thích của bạn.

./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 tệp tiêu đề API mà bạn cần để định cấu hình Thread network:

#include <openthread/dataset_ftd.h>

HÀNH ĐỘNG: Thêm phần 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 câu lệnh #if bất kỳ. Hàm này sẽ được xác định 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 của Thread.

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

HÀNH ĐỘNG: Thêm lệnh gọi để bật giao diện và ngăn xếp 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 cách 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ư đã nêu chi tiết trong hàm này, các tham số mạng Thread mà chúng tôi đang sử dụng cho ứng dụng này là:

  • Kênh = 15
  • PAN ID = 0x2222
  • ID PAN mở rộng = C0DE1AB5C0DE1AB5
  • Khóa mạng = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Tên mạng = OTCodelab

Ngoài ra, đây là nơi chúng tôi giảm Bộ chọn định tuyến để các thiết bị của chúng tôi thay đổi vai trò nhanh hơn cho mục đích minh hoạ. Lưu ý rằng điều này chỉ được thực hiện nếu nút là FTD (Full Thread Device). Hãy tìm hiểu thêm trong bước tiếp theo.

9. API: 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 sử dụng các API này trong quá trình triển khai sản xuấ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) cần để Thiết bị cuối tự quảng bá lên Bộ định tuyến. Giá trị mặc định của giá trị này là 120, theo Đặc tả luồng. Để dễ dàng sử dụng trong Lớp học lập trình này, chúng ta sẽ thay đổi thành 20 để bạn không phải chờ quá lâu để nút Thread thay đổi vai trò.

Lưu ý: Thiết bị MTD không trở thành bộ định tuyến và tính năng hỗ trợ chức năng như otThreadSetRouterSelectionJitter không có trong bản dựng MTD. Sau này, chúng ta cần chỉ định tuỳ chọn CMake -DOT_MTD=OFF, nếu không 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 tiền xử lý của 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 tạo ứng dụng, bạn cần thực hiện một số cập nhật nhỏ đối với 3 tệp CMake. Các hàm này được hệ thống xây dựng sử dụng để 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 NordicSemiconductor CMakeLists.txt để đả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 soạn thảo văn bản mà bạn muốn dùng và thêm các dòng sau trong 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 soạn thảo văn bản yêu thích của bạn và thêm tệp vào mục 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 NordicSemiconductor CMakeLists.txt để tệp này đi kèm với bản dựng thư viện của các 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 NordicSemiconductor.

Mở ./third_party/NordicSemiconductor/CMakeLists.txt trong trình soạn thảo văn bản yêu thích của bạn và thêm tệp vào mục 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 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 bo mạch nhà 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ị chuỗi đầy đủ (FTD).

Tạo OpenThread

Xây dựng 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

Điều hướng đến thư mục có tệp nhị phân OpenThread FTD CLI và chuyển đổi tệp sang định dạng thập lục phân bằng Chuỗi công cụ nhúng ARM:

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

Lướt bảng

Đưa tệp ot-cli-ftd.hex vào mỗi bo mạch nRF52840.

Cắm cáp USB vào cổng gỡ lỗi Micro-USB bên cạnh chân cắm nguồn bên ngoài trên bo mạch nRF52840, sau đó cắm vào máy Linux của bạn. Đặt đúng LED5.

20a3b4b480356447.png

Như trước đây, hãy lưu ý số sê-ri của bo mạch nRF52840:

c00d519ebec7e5f0.jpeg

Điều hướng đến vị trí của Công cụ dòng lệnh nRFx và flash tệp hex OpenThread CLI FTD lên bo mạch 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 trong nháy mắt. 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 bước này trên bảng điều khiển dành 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ách giống nhau và lệnh để flash là như 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ẽ được chiếu sáng trên mỗi bảng. Bạn thậm chí có thể thấy công tắc 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

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

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

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

  • LED1 = Lãnh đạo
  • LED2 = Bộ định tuyến
  • LED3 = Thiết bị Cuối

Vai trò cũng thay đổi theo đèn LED. Bạn có thể đã thấy các thay đổi này trên một hoặc hai bo mạch trong vòng 20 giây của mỗi thiết bị đang bật nguồn.

Phát đa hướng UDP

Khi nhấn Nút1 trên một bảng, một thông báo UDP được gửi đến địa chỉ phát đa hướng cục bộ lưới, bao gồm tất cả các nút khác trong mạng Luồng. Để trả lời 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.png.

9bbd96d9b1c63504.png.

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

Thiết bị bạn vừa flash là một loại Thiết bị tạo chuỗi đầy đủ (FTD) cụ thể được gọi là Thiết bị kết thúc đủ điều kiện của bộ định tuyến (REED). Điều này có nghĩa là các phím tắt 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ừ Thiết bị cuối với Bộ định tuyến.

Thread có thể hỗ trợ tối đa 32 Router, nhưng cố gắng giữ số lượng Router từ 16 đến 23. Nếu REED đính kèm dưới dạng Thiết bị cuối và số lượng Bộ định tuyến dưới 16, thì thiết bị này sẽ tự động quảng bá tới Bộ định tuyến. Thay đổi này sẽ xảy ra tại 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 cũng có một Trưởng nhóm, là một Router chịu trách nhiệm quản lý tập hợp Router trong mạng Thread. Với tất cả các thiết bị bật, sau 20 giây, một trong số chúng sẽ là Leader (LED1 bật) và hai thiết bị còn lại sẽ là Router (LED2 bật).

4e1e885861a66570.png.

Xóa người lãnh đạo

Nếu Người lãnh đạo bị xóa khỏi mạng Chủ đề, một Bộ định tuyến khác tự thăng hạng lên Nhà lãnh đạo để đảm bảo mạng vẫn có Người lãnh đạo.

Tắt bảng điều khiển Thủ lĩnh (bảng có đèn LED1 sáng) bằng công tắc Nguồn. Chờ 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à LED1 (Máy lãnh đạo) sẽ bật. Thiết bị này hiện là người lãnh đạo mạng Thread.

4c57c87adb40e0e3.png.

Bật lại Board ban đầu. Thiết bị sẽ tự động tham gia lại mạng Chuỗi dưới dạng Thiết bị cuối (LED3 sáng). Trong vòng 20 giây (Bộ định tuyến lựa chọn bộ định tuyến), nó tự thăng cấp thành Bộ định tuyến (LED2 sáng).

5f40afca2dcc4b5b.png.

Đặt lại bảng

Tắt cả ba bảng, sau đó bật lại và quan sát đèn LED. Bo mạch đầu tiên được bật nguồn sẽ bắt đầu với vai trò Thủ lĩnh (LED1 sáng) — Bộ định tuyến đầu tiên trong mạng Chuỗi tự động trở thành Thủ lĩnh.

Hai bo mạch 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á tới Bộ định tuyến (LED2 sáng) trong vòng 20 giây.

Phân vùng mạng

Nếu bảng của bạn không nhận đủ năng lượng hoặc kết nối radio giữa chúng yếu, mạng Thread có thể bị tách thành các phân vùng và bạn có thể có nhiều thiết bị hiển thị với tư cách là Thủ lĩnh.

Luồng có khả năng tự phục hồi, vì vậy, cuối cùng các phân vùng sẽ hợp nhất lại thành một phân vùng duy nhất với một biến thể dẫn đầu.

14. Demo: Gửi đa hướng UDP

Nếu tiếp tục từ 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 bất kỳ bảng nào và nhấn Button1. LED4 trên tất cả bo mạch khác trong mạng luồng chạy ứng dụng sẽ chuyển đổi trạng thái của chúng. Nếu tiếp tục từ bài tập trước, các bài tập đó sẽ bật.

f186a2618fdbe3fd.png.

Nhấn lại Nút1 cho bảng đó. Đèn LED4 trên tất cả các bo mạch khác sẽ bật/tắt lại.

Nhấn Button1 trên một board khác và quan sát cách LED4 bật/tắt trên các board khác. Nhấn Nút1 trên một trong các bo mạch hiện đang bật LED4. Đèn LED4 vẫn bật trên bo mạch đó nhưng vẫn bật.

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 hơn một Người lãnh đạo trong số đó, 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 Nút1 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 Chủ đề được phân vùng), LED4 trên các bảng khác sẽ không sáng khi phản hồi. Nếu điều này xảy ra, hãy đặt lại bảng. Lý tưởng là bảng sẽ định dạng lại một mạng Luồng 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 bo mạch phát triển Nordic 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 của OpenThread
  • Cách gửi tin nhắn UDP đến tất cả các thiết bị trong mạng chuỗi
  • Cách sửa đổi tệp 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ử 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 và kết nối các đèn LED RGB bên ngoài thay đổi màu sắc 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ị từ một thao tác nhấn nút, hãy sử dụng API Router/Leader để định vị và ping một thiết bị riêng lẻ
  • Kết nối mạng lưới với Internet bằng cách sử dụng Bộ định tuyến đường viền OpenThread và phát đa hướng từ bên ngoài mạng Thread để chiếu sáng đèn LED

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

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

Tham khảo: