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 các 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ó bảo mật MAC, Thiết lập liên kết mạng lưới và Định tuyến mạng 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 những 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à nạp chương trình vào 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 để nạp chương trình vào 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 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 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à chế độ 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ẻ 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 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 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 phụ thuộc vào nhu cầu của ứng dụng. Nếu không cần dùng trong quá trình triển khai hàm, bạn có thể dùng macro OT_UNUSED_VARIABLE từ 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ề điều này sau.
5. Triển khai lớp trừu tượng nền tảng GPIO
Ở 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 dành riêng cho 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 mesh extender 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 tệp tiêu đề.
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 tạo 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 với 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, Mesh Extender, End Device. Chúng ta có thể chỉ định các giá trị này cho đèn LED như sau:
- LED1 = Leader
- LED2 = Bộ mở rộng sóng
- 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 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 tệp tiêu đề.
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 cho OpenThread biết rằng cần 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ỉ đ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 tệp tiêu đề.
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 phần 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 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 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 tạo 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ề API UDP.
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. Các hàm 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 trong Tài liệu tham khảo 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 nguồn. Để 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.
./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 sẽ 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);
VIỆC CẦN LÀM: 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 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 tôi giảm Router Selection Jitter (Độ trễ khi chọn bộ định tuyến), vì vậy, các thiết bị của chúng tôi sẽ 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 dù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á thành Bộ mở rộng mạng lưới. Giá trị mặc định của 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ộ mở rộng mạng 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. Định nghĩa này nằm trong một 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 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 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 bản dựng 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 và chuyển đổi tệp này 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 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 "Đưa nhanh 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. Hãy nhớ sử dụng số sê-ri riêng 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ộ mở rộng sóng
- 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 một bảng, một thông báo UDP sẽ được gửi đến địa chỉ truyền tin đ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ị có đầy đủ chức năng (FTD) cụ thể, được gọi là Thiết bị có khả năng mở rộng (ECD). Điều này có nghĩa là chúng có thể hoạt động dưới dạng Bộ mở rộng mạng hoặc ECD dự phòng, đồng thời có thể tự chuyển từ ECD dự phòng sang Bộ mở rộng mạng.
Thread có thể hỗ trợ tối đa 32 Bộ mở rộng mạng, nhưng cố gắng giữ số lượng Bộ mở rộng mạng trong khoảng từ 16 đến 23. Nếu ECD gắn dưới dạng Thiết bị đầu cuối và số lượng Bộ mở rộng mạng lưới dưới 16, thì ECD sẽ tự động nâng cấp lên Bộ mở rộng mạng lưới. 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 Thiết bị dẫn đầu, là một Bộ mở rộng mạng lưới có trách nhiệm quản lý bộ Bộ mở rộng mạng lưới 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ị chính (LED1 bật) và hai thiết bị còn lại sẽ là Thiết bị mở rộng mạng lưới (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ộ mở rộng mạng sẽ tự động chuyển 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ộ mở rộng mạng lưới) sẽ tắt và LED1 (Thiết bị chính) sẽ bật. Thiết bị này hiện là Thiết bị dẫn đầu của mạng 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ộ mở rộng mạng lưới (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 (Đèn LED1 sáng) – Mesh Extender đầu tiên trong mạng Thread sẽ tự động trở thành Leader.
Hai bảng còn lại ban đầu 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ộ mở rộng mạng lưới (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 các bảng mạch yếu, thì mạng 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à Trưởng nhóm.
Luồng có khả năng tự phục hồi, vì vậy, các phân vùng cuối cù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 gói tin đa hướng 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 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 cùng một 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 bật cho bảng đó nhưng sẽ bật/tắt cho 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 sắc dựa trên vai trò của Bộ mở rộng mạng
- 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ả 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 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 khác 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: