1. Giới thiệu

OpenThread do Nest phát hành là một cách 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ệ được dùng trong các sản phẩm của Nest cho nhà phát triển nhằm đẩy nhanh quá trình phát triển sản phẩm cho nhà thông minh.
Quy cách Thread xác định một giao thức giao tiếp không dây đáng tin cậy, an toàn và tiêu thụ ít điện năng giữa các thiết bị 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 có tính năng bảo mật MAC, Thiết lập liên kết mạng và Định tuyến mạng.
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ị, cũng như 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.

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 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 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 Nordic Semiconductor nRF52840
- 3 cáp USB sang Micro-USB để kết nối các bảng
- 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 Nordic nRF5x
- Phần mềm Segger J-Link
- OpenThread
- Git
Trừ phi 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 Ghi công theo Creative Commons 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 Thread bằng các bảng nRF52840 và OpenThread. Lớp học lập trình này sẽ:
- Liệt kê tất cả phần mềm bạn cần để tạo và cài đặt
- Hướng dẫn bạn cách tạo OpenThread và flash trên các bo mạch Nordic nRF52840
- Minh hoạ những điều cơ bản về mạng Thread
Không có chế độ thiết lập môi trường nào cần thiết để tạo OpenThread và flash các bảng được trình bày chi tiết trong Lớp học lập trình này – chỉ có hướng dẫn cơ bản để flash các bảng. Giả sử bạn đã hoàn thành Lớp học lập trình 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 i386 hoặc x86 để flash tất cả các bảng phát triển Thread. Tất cả các bước đều được kiểm thử trên Ubuntu 14.04.5 LTS (Trusty Tahr).
Bo mạch Nordic Semiconductor nRF52840
Lớp học lập trình này sử dụng 3 bo mạch PDK nRF52840.

Cài đặt phần mềm
Để tạo và flash 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. Nếu đã hoàn thành Lớp học lập trình Xây dựng mạng lưới Thread theo yêu cầu, thì bạn đã cài đặt mọi thứ cần thiết. Nếu không, hãy hoàn thành 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à flash 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ể dùng làm điểm xuất phát cho Lớp học lập trình này.
Tạo bản sao kho lưu trữ ví dụ OpenThread Nordic nRF528xx và tạo OpenThread:
$ git clone --recursive https://github.com/openthread/ot-nrf528xx $ cd ot-nrf528xx $ ./script/bootstrap
4. Những điều cơ bản về OpenThread API
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à cấp độ nền tảng để sử dụng trong các ứng dụng của bạn:
- Thông tin và quyền kiểm soát phiên bản 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ò Người uỷ quyề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 tín hiệu gây nhiễu
Thông tin tham khảo về tất cả các API OpenThread có tại openthread.io/reference.
Sử dụng API
Để sử dụng một API, hãy thê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 đó, hãy gọi hàm mà bạn muốn.
Ví dụ: ứng dụng ví dụ 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 trúc mà bạn sẽ 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 sẽ đạ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 thực hiện các lệnh gọi API OpenThread.
Ví dụ: phiên bản OpenThread được khởi tạo trong hàm main() của ứng dụng ví dụ 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;
}
Hàm dành riêng cho từng 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 đó 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 hàm này trong một tệp nguồn dành riêng cho nền tảng. Bằng cách trừu tượng hoá theo cách này, bạn có thể sử dụng cùng một tiêu đề hàm cho các nền tảng ví dụ khác.
Ví dụ: các hàm GPIO mà chúng ta sẽ dùng để kết nối với các nút và đèn LED nRF52840 phải được khai báo trong openthread-system.h.
Mở tệp ./openthread/examples/platforms/openthread-system.h trong trình chỉnh sửa văn bản mà bạn muốn.
./openthread/examples/platforms/openthread-system.h
HÀNH ĐỘNG: Thêm các khai báo hàm GPIO dành riêng cho nền tảng.
Thêm các 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 khai báo hàm otSysButtonProcess sử dụng một otInstance. Bằng cách đó, ứng dụng có thể truy cập vào thông tin về phiên bản OpenThread khi cần, nếu cần. Điều này hoàn toàn phụ thuộc vào nhu cầu của ứng dụng. Nếu không cần đến macro này trong quá trình triển khai hàm, bạn có thể dùng macro OT_UNUSED_VARIABLE trong OpenThread API để ngăn chặn lỗi bản dựng liên quan đến 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ề vấn đề này sau.
5. Triển khai lớp trừu tượng nền tảng GPIO
Trong bước trước, chúng ta đã xem xét các 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 vào các nút và đèn LED trên bảng phát triển nRF52840, bạn cần triển khai những 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 động các chân và chế độ GPIO
- Kiểm soát điện áp trên một chân
- Bật các gián đoạn GPIO và đăng ký một 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à các thành phần trừu tượng giữa các giá trị và biến cụ thể của nRF52840 đượ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 Infocenter của Nordic Semiconductor.
HÀNH ĐỘNG: Thêm các phần đầu trang.
Tiếp theo, hãy thêm các tiêu đề mà 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 các hàm callback 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 chức năng nhấn nút được khởi tạo (sau này trong tệp này).
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 truyề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ả cá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 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 LED4 khi thiết bị nhận được một thông báo UDP truyền tin đ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 các hàm để khởi chạy và xử lý các lần nhấn nút.
Hàm đầu tiên khởi tạo bảng cho một lần nhấn nút và hàm thứ hai gửi thông báo UDP truyền tin đa 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 gpio.c tệp.
6. API: Phản ứng trước các thay đổi về vai trò của thiết bị
Trong ứng dụng của chúng tôi, chúng tôi 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 các vai trò sau: Leader (Trưởng nhóm), Router (Bộ định tuyến), End Device (Thiết bị cuối). Chúng ta có thể chỉ định các giá trị này cho đèn LED như sau:
- LED1 = Leader
- LED2 = Bộ định tuyến
- LED3 = Thiết bị đầu 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 đèn LED phù hợp để phản hồi. Chúng ta sẽ dùng phiên bản OpenThread cho phần đầu tiên và lớp trừu tượng 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 mà bạn muốn.
./openthread/examples/apps/cli/main.c
HÀNH ĐỘNG: Thêm các phần đầu trang.
Trong phần includes của tệp main.c, hãy thêm các tệp tiêu đề API mà bạn sẽ 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 khai báo hàm trình xử lý cho thay đổi trạng thái của thực thể OpenThread.
Thêm khai báo này vào main.c, sau khi tiêu đề bao gồm và trước mọi câu lệnh #if. 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 một lệnh gọi lại đăng ký cho hàm 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 yêu cầu OpenThread gọi hàm handleNetifStateChange bất cứ khi nào trạng thái của phiên bản 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 phiên bản OpenThread và nếu cờ này đã thay đổi, thì sẽ 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 truyền tin đ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 Button1 trên một bảng. Để xác nhận đã nhận được thông báo, chúng tôi sẽ bật LED4 trên các bảng khác để phản hồi.
Để bật chức năng này, ứng dụng cần:
- Khởi tạo một kết nối UDP khi khởi động
- Có thể gửi một thông báo UDP đến địa chỉ truyền tin đa hướng cục bộ của mạng lưới
- Xử lý thông báo UDP đến
- Bật/tắt 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 chỉnh sửa văn bản mà bạn muốn.
./openthread/examples/apps/cli/main.c
HÀNH ĐỘNG: Thêm các phần đầu trang.
Trong phần bao gồm ở đầu tệp main.c, hãy thêm các tệp tiêu đề API mà bạn sẽ cần cho tính năng UDP truyền tin đ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 otEXPECT và otEXPECT_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 hiệu quả.
HÀNH ĐỘNG: Thêm các định nghĩa và hằng số:
Trong tệp main.c, sau phần nội dung bao gồm và trước mọi câu lệnh #if, hãy thêm các hằng số và định nghĩa dành riêng cho UDP:
#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 tin đa hướng cục bộ của mạng lưới. Mọi thông báo gửi đến địa chỉ này sẽ được gửi đến tất cả các Thiết bị có đầy đủ luồng trong mạng. Hãy xem bài viết Truyền tin đa hướng trên openthread.io để biết thêm thông tin về tính năng hỗ trợ truyền tin đa hướng trong OpenThread.
HÀNH ĐỘNG: Thêm 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 để biểu thị 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 các lệnh gọi để khởi động 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 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 tạo 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 một ổ cắm UDP được khởi chạy khi ứng dụng khởi động. Nếu không có thông tin này, 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 nút hay không. Nếu có, hàm này sẽ gọi trình xử lý (handleButtonInterrupt) đã được đặt ở bước trên.
otSysButtonProcess(instance);
HÀNH ĐỘNG: Triển khai Trình xử lý ngắt nút.
Trong main.c, hãy thêm quá trình 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 động 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 sẽ mở ổ cắm và đăng ký một hàm callback (handleUdpReceive) cho thời điểm 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 lựa chọn khác về giao diện mạng, hãy tham khảo chế độ liệt kê otNetifIdentifier trong Tài liệu tham khảo về UDP API.
HÀNH ĐỘNG: Triển khai tính năng nhắn tin 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);
}
}
Lưu ý các macro otEXPECT và otEXPECT_ACTION. Những điều này đảm bảo rằng thông báo UDP hợp lệ và được phân bổ chính xác trong vùng đệm. Nếu không, hàm sẽ xử lý lỗi một cách hiệu quả bằng cách chuyển đến khối exit, nơi hàm giải phóng vùng đệm.
Hãy xem phần IPv6 và UDP References (Tài liệu tham khảo về IPv6 và UDP) trên openthread.io để biết thêm thông tin về các hàm dùng để khởi động UDP.
HÀNH ĐỘNG: Triển khai quy trình xử lý thông báo UDP.
Trong main.c, hãy thêm việc triển khai hàm handleUdpReceive sau hàm sendUdp mà bạn vừa thêm. Hàm này chỉ cần 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ễ dàng minh hoạ, chúng tôi muốn các thiết bị của mình bắt đầu Thread ngay lập tức và cùng nhau tham gia vào một mạng khi được bật. Để làm việc này, chúng ta 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 đăng nhập 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 của mạng được tích hợp trong OpenThread, để giúp ứng dụng của chúng ta an toàn hơn và giới hạn các nút Thread trong mạng của chúng ta chỉ cho những nút đang chạy ứng dụng.
Một lần nữa, hãy mở tệp ./openthread/examples/apps/cli/main.c trong trình chỉnh sửa văn bản mà bạn muốn dùng.
./openthread/examples/apps/cli/main.c
HÀNH ĐỘNG: Thêm phần đầu trang.
Trong phần includes ở đầ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 khai báo hàm để thiết lập cấu hình mạng.
Thêm khai báo này vào main.c, sau khi tiêu đề bao gồm và trước mọi câu lệnh #if. 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 mạng Thread.
/* Override default network credentials */ setNetworkConfiguration(instance);
HÀNH ĐỘNG: Thêm các 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 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ư đã nêu chi tiết trong hàm, các thông số mạng Thread mà chúng ta đang sử dụng cho ứng dụng này là:
- Kênh = 15
- PAN ID = 0x2222
- Mã PAN mở rộng = C0DE1AB5C0DE1AB5
- Khoá mạng = 1234C0DE1AB51234C0DE1AB51234C0DE
- Network Name = OTCodelab
Ngoài ra, đây là nơi chúng ta giảm Router Selection Jitter (Độ trễ khi chọn bộ định tuyến), để các thiết bị thay đổi vai trò nhanh hơn cho mục đích minh hoạ. Xin lưu ý rằng thao tác này chỉ được thực hiện nếu nút là FTD (Thiết bị có đầy đủ chức năng của giao thức Thread). Chúng ta sẽ tìm hiểu thêm về vấn đề này ở 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 được sửa đổi cho mục đích minh hoạ hoặc thử nghiệm. Bạn không nên sử dụng các API này trong quá trình triển khai phiên bản chính thức của một ứng dụng sử dụng OpenThread.
Ví dụ: hàm otThreadSetRouterSelectionJitter điều chỉnh thời gian (tính bằng giây) mà một Thiết bị cuối cần để tự nâng cấp lên Bộ định tuyến. Giá trị mặc định cho giá trị này là 120, theo Quy cách về luồng. Để dễ sử dụng trong Lớp học lập trình này, chúng ta sẽ thay đổi giá trị này thành 20 để bạn không phải đợi quá lâu cho một nút Thread thay đổi vai trò.
Lưu ý: Các 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 chức năng như otThreadSetRouterSelectionJitter. Sau đó, chúng ta cần chỉ định lựa chọn CMake -DOT_MTD=OFF, nếu không, chúng ta sẽ gặp phải lỗi khi tạo.
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 chỉ thị 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. Nội dung cập nhật về CMake
Trước khi tạo ứng dụng, bạn cần thực hiện một số bản cập nhật nhỏ cho 3 tệp CMake. Hệ thống xây dựng dùng các thư viện 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 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, rồi 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, 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 NordicSemiconductor để tệp này được đưa vào bản dựng thư viện của trình điều khiển Nordic.
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 chỉnh sửa văn bản mà bạn muốn, 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ị
Sau khi hoàn tất mọi nội dung cập nhật mã, bạn đã sẵn sàng tạo và cài đặt ROM ứng dụng vào cả 3 bảng phát triển Nordic nRF52840. Mỗi thiết bị sẽ hoạt động như một Thiết bị Thread đầy đủ (FTD).
Tạo OpenThread
Tạo các tệp nhị phân FTD OpenThread 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 CLI FTD OpenThread rồi chuyển đổi tệp đó sang định dạng thập lục phân bằng ARM Embedded Toolchain:
$ cd build/bin $ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex
Cài đặt ROM cho bảng
Nạp 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 bo mạch nRF52840, rồi cắm cáp đó vào máy Linux. Đặt đúng cách, LED5 sẽ bật.

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

Chuyển đến vị trí của nRFx Command Line Tools (Công cụ dòng lệnh nRFx) rồi cài đặt ROM tệp hex OpenThread CLI FTD vào bo mạch nRF52840 bằng số sê-ri của bo mạch:
$ 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 chốc lát khi nhấp nháy. Kết quả sau đây sẽ đượ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 "Flash the boards" (Nháy các bảng) này cho 2 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 một cách và lệnh để flash cũng giống nhau, ngoại trừ số sê-ri của bảng. Nhớ sử dụng số sê-ri riêng biệt của từng 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. Ngay sau khi nhấp nháy, bạn thậm chí có thể thấy đèn LED sáng chuyển từ 3 sang 2 (hoặc 2 sang 1) (tính năng thay đổi vai trò của thiết bị).
12. Chức năng của ứng dụng
Giờ đây, cả 3 bo mạch nRF52840 đều sẽ đượ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ó 2 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 mạch phản ánh vai trò hiện tại của nút Thread:
- LED1 = Leader
- LED2 = Bộ định tuyến
- LED3 = Thiết bị đầu cuối
Khi vai trò thay đổi, đèn LED sáng cũng thay đổi. Bạn sẽ 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.
UDP Multicast
Khi Button1 được nhấn trên bảng, một thông báo UDP sẽ được gửi đến địa chỉ phát đa hướng cục bộ của mạng lưới, bao gồm tất cả các nút khác trong mạng Thread. Khi 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 mỗi bảng cho đến khi nhận được một thông báo UDP khác.


13. Bản minh hoạ: Quan sát các thay đổi về vai trò của thiết bị
Các thiết bị mà bạn đã cài đặt là một loại Thiết bị Thread đầy đủ (FTD) cụ thể, được gọi là Thiết bị cuối đủ điều kiện làm bộ định tuyến (REED). Điều này có nghĩa là chúng có thể hoạt động như một Bộ định tuyến hoặc Thiết bị cuối và có thể tự nâng cấp từ Thiết bị cuối lên Bộ định tuyến.
Thread có thể hỗ trợ tối đa 32 Bộ định tuyến, nhưng cố gắng duy trì số lượng Bộ định tuyến trong khoảng từ 16 đến 23. Nếu một REED được đính kèm dưới dạng Thiết bị cuối và số lượng Bộ định tuyến dưới 16, thì REED đó sẽ tự động nâng cấp lên Bộ định tuyến. Thay đổi này sẽ diễn ra vào một thời điểm ngẫu nhiên trong 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 Leader (Trưởng nhóm), là một Bộ định tuyến chịu trách nhiệm quản lý nhóm Bộ định tuyến trong mạng Thread. Khi tất cả các thiết bị đều bật, sau 20 giây, một trong số đó 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).

Xoá người lãnh đạo
Nếu Bộ định tuyến chính bị xoá khỏi mạng Thread, một Bộ định tuyến khác sẽ tự thăng cấp thành Bộ định tuyến chính để đảm bảo mạng vẫn có Bộ định tuyến chính.
Tắt Bảng điều khiển chí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 bảng còn lại, LED2 (Bộ định tuyến) sẽ tắt và LED1 (Trưởng nhóm) sẽ bật. Thiết bị này hiện là Thiết bị dẫn đầu của mạng giao thức Thread.

Bật lại Bảng xếp hạng ban đầu. Thiết bị sẽ tự động kết nối lại với mạng Thread dưới dạng Thiết bị đầu cuối (LED3 sáng). Trong vòng 20 giây (Độ trễ chọn bộ định tuyến), thiết bị sẽ tự quảng bá thành Bộ định tuyến (LED2 sáng).

Đặt lại bảng
Tắt cả 3 bảng, sau đó bật lại và quan sát các đèn LED. Bo mạch đầu tiên được bật nguồn sẽ bắt đầu ở vai trò Leader (LED1 sáng) – Router đầu tiên trong mạng Thread sẽ tự động trở thành Leader.
Ban đầu, 2 bảng còn lại kết nối với mạng dưới dạng Thiết bị đầu cuối (LED3 sáng) nhưng sẽ tự nâng cấp lên Bộ định tuyến (LED2 sáng) trong vòng 20 giây.
Phân vùng mạng
Nếu các bảng mạch không nhận đủ nguồn điện hoặc kết nối vô tuyến giữa chúng yếu, thì mạng giao thức Thread có thể phân chia thành các phân vùng và bạn có thể có nhiều Thiết bị hiển thị là Thủ lĩnh.
Giao thức Thread có khả năng tự khắc phục, vì vậy, các phân vùng sẽ 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 truyền tin UDP
Nếu bạn tiếp tục từ bài tập trước, thì 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ẽ chuyển đổi trạng thái. Nếu bạn tiếp tục từ bài tập trước, thì các thiết bị này sẽ bật.

Nhấn lại vào Nút 1 cho bảng đó. LED4 trên tất cả các bảng khác sẽ bật/tắt lại.
Nhấn Button1 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 các bảng mà đèn LED 4 hiện đang bật. LED4 vẫn sáng trên bảng đó nhưng sẽ bật/tắt trên các bảng khác.

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 Leader trong số đó, thì kết quả của thông báo truyền tin 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), thì LED4 trên các bảng khác sẽ không sáng để phản hồi. Nếu điều này xảy ra, hãy đặt lại các bảng. Tốt nhất là các bảng sẽ tạo thành một mạng Thread duy nhất và hoạt động nhắn tin UDP sẽ hoạt động bình thường.
15. Xin chúc mừng!
Bạn đã tạo một ứng dụng sử dụng các API OpenThread!
Giờ thì bạn đã biết:
- Cách lập trình các nút và đèn LED trên bảng 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 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ử các bài tập sau:
- Sửa đổi mô-đun GPIO để sử dụng các chân GPIO thay vì đèn LED trên bo mạch và kết nối các đèn LED RGB bên ngoài 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 ví dụ khác
- Thay vì dùng truyền tin đa hướng để ping tất cả các thiết bị khi nhấn nút, hãy dùng Router/Leader API để xác định vị trí 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 biên OpenThread và truyền đa hướng các mạng lưới đó từ bên ngoài mạng lưới Thread để bật đèn LED
Tài liệu đọc thêm
Hãy xem openthread.io và GitHub để biết nhiều tài nguyên về OpenThread, bao gồm:
- Nền tảng được hỗ trợ – khám phá tất cả nền tảng hỗ trợ OpenThread
- Tạo OpenThread – thông tin chi tiết hơn về cách tạo và định cấu hình OpenThread
- Thread Primer – một tài liệu tham khảo hữu ích về các khái niệm của Thread
Tài liệu tham khảo: