1. Giới thiệu
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 kết nối mạng Thread®. Nest đã phát hành OpenThread để cung cấp rộng rãi công nghệ được sử dụng trong các sản phẩm 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.
Thông số kỹ thuật của Thread xác định giao thức giao tiếp giữa các thiết bị không dây dựa trên IPv6, đáng tin cậy, an toàn và tiêu thụ điện năng thấp 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 cả IPv6, 6LoWPAN, IEEE 802.15.4 với tính năng 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 để khởi động 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 nRF52840 của Nordic Semiconductor
- 3 cáp USB sang Micro-USB để kết nối các bảng
- 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ó 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 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 Tạo mạng Thread bằng Bảng mạch nRF52840 và OpenThread. Lớp học này:
- Thông tin chi tiết về tất cả phần mềm bạn cần để tạo bản dựng và cài đặt ROM
- Hướng dẫn bạn cách tạo OpenThread và cài đặt ROM trên các bo mạch Nordic nRF52840
- Minh hoạ các kiến thức cơ bản về mạng Luồng
Lớp học lập trình này không trình bày chi tiết về việc thiết lập môi trường cần thiết để tạo OpenThread và cài đặt ROM cho các bảng mạch, mà chỉ hướng dẫn cơ bản về cách cài đặt ROM cho các bảng mạch. Giả định rằng bạn đã hoàn thành lớp học lập trình Tạo mạng luồng.
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 để cài đặt ROM cho tất cả các bo mạch 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).
Bảng mạch nRF52840 của Nordic Semiconductor
Lớp học lập trình này sử dụng ba bảng PDK nRF52840.
Cài đặt phần mềm
Để tạo và cài đặt ROM OpenThread, bạn cần cài đặt SEGGER J-Link, công cụ Dòng lệnh nRF5x, Chuỗi công cụ ARM GNU và nhiều gói Linux. Nếu đã hoàn thành lớp học lập trình Tạo mạng luồng theo yêu cầu, bạn sẽ đã 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à cài đặt ROM OpenThread vào các bảng phát triển nRF52840.
3. Sao chép kho lưu trữ
OpenThread đi kèm với mã ứ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.
Sao chép 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. Kiến thức cơ bản về API OpenThread
Các API công khai của OpenThread nằm ở ./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 Luồng và cấp nền tảng để sử dụng trong ứng dụng của bạn:
- Thông tin và quyền kiểm soát thực thể 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ò 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ư Chế độ giám sát trẻ em và Phát hiện âm thanh gây nhiễu
Bạn có thể xem thông tin tham khảo về tất cả API OpenThread tại openthread.io/reference.
Sử dụng API
Để sử dụng một API, hãy đưa tệp tiêu đề của API đó vào một trong các tệp ứng dụng của bạn. Sau đó, hãy gọi hàm mong muốn.
Ví dụ: ứng dụng mẫu CLI đi kèm với OpenThread sử dụng các tiêu đề API sau:
./openthread/examples/apps/cli/main.c
#include <openthread/config.h> #include <openthread/cli.h> #include <openthread/diag.h> #include <openthread/tasklet.h> #include <openthread/platform/logging.h>
Phiên bản OpenThread
Cấu trúc otInstance
là một 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 thực thể tĩnh của thư viện OpenThread và cho phép người dùng thực hiện các lệnh gọi API OpenThread.
Ví dụ: thực thể OpenThread được khởi tạo trong hàm main()
của ứng dụng ví dụ về 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 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
, sử dụng không gian tên otSys
cho tất cả các hàm. Sau đó, triển khai các lớp 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ẽ 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 nội dung khai báo hàm GPIO dành riêng cho nền tảng.
Thêm các phần khai báo hàm sau đâ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 nội dung 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ề thực thể OpenThread khi nhấn nút (nếu cần). Tất cả phụ thuộc vào nhu cầu của ứng dụng. Nếu không cần macro 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 để 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 tính năng trừu tượng nền tảng GPIO
Trong bước trước, chúng ta đã xem xét các nội dung khai báo hàm dành riêng cho nền tảng trong ./openthread/examples/platforms/openthread-system.h
có thể dùng cho GPIO. Để truy cập 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 các hàm đó cho nền tảng nRF52840. Trong mã này, bạn sẽ thêm các hàm:
- Khởi chạy các chế độ và chân GPIO
- Kiểm soát điện áp trên một chân
- Bật các ngắt GPIO và đăng ký lệnh gọi lại
Trong thư mục ./src/src
, hãy tạo một tệp mới có tên gpio.c
. Trong tệp mới này, hãy thêm nội dung sau.
./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ị và biến dành riêng cho nRF52840 được sử dụng ở cấp ứng dụng OpenThread.
/** * @file * This file implements the system abstraction for GPIO and GPIOTE. * */ #define BUTTON_GPIO_PORT 0x50000300UL #define BUTTON_PIN 11 // button #1 #define GPIO_LOGIC_HI 0 #define GPIO_LOGIC_LOW 1 #define LED_GPIO_PORT 0x50000300UL #define LED_1_PIN 13 // turn on to indicate leader role #define LED_2_PIN 14 // turn on to indicate router role #define LED_3_PIN 15 // turn on to indicate child role #define LED_4_PIN 16 // turn on to indicate UDP receive
Để biết thêm thông tin về các nút và đèn LED nRF52840, hãy xem Trung tâm thông tin về 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 những gì bạn cần cho chức năng GPIO.
/* Header for the functions defined here */ #include "openthread-system.h" #include <string.h> /* Header to access an OpenThread instance */ #include <openthread/instance.h> /* Headers for lower-level nRF52840 functions */ #include "platform-nrf5.h" #include "hal/nrf_gpio.h" #include "hal/nrf_gpiote.h" #include "nrfx/drivers/include/nrfx_gpiote.h"
HÀNH ĐỘNG: Thêm hàm gọi lại và hàm ngắt cho Nút 1.
Tiếp theo, hãy thêm mã sau. 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 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 sử dụng trong hàm.
/* Declaring callback function for button 1. */ static otSysButtonCallback sButtonHandler; static bool sButtonPressed; /** * @brief Function to receive interrupt and call back function * set by the application for button 1. * */ static void in_pin1_handler(uint32_t pin, nrf_gpiote_polarity_t action) { OT_UNUSED_VARIABLE(pin); OT_UNUSED_VARIABLE(action); sButtonPressed = true; }
HÀNH ĐỘNG: Thêm một hàm để định cấu hình đèn LED.
Thêm mã này để định cấu hình chế độ và trạng thái của tất cả đèn LED trong quá trình khởi chạy.
/** * @brief Function for configuring: PIN_IN pin for input, PIN_OUT pin for output, * and configures GPIOTE to give an interrupt on pin change. */ void otSysLedInit(void) { /* Configure GPIO mode: output */ nrf_gpio_cfg_output(LED_1_PIN); nrf_gpio_cfg_output(LED_2_PIN); nrf_gpio_cfg_output(LED_3_PIN); nrf_gpio_cfg_output(LED_4_PIN); /* Clear all output first */ nrf_gpio_pin_write(LED_1_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_2_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_3_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_4_PIN, GPIO_LOGIC_LOW); /* Initialize gpiote for button(s) input. Button event handlers are set in the application (main.c) */ ret_code_t err_code; err_code = nrfx_gpiote_init(); APP_ERROR_CHECK(err_code); }
HÀNH ĐỘNG: Thêm một hàm để đặt chế độ của đèn LED.
Hàm này sẽ được dùng khi vai trò của thiết bị thay đổi.
/** * @brief Function to set the mode of an LED. */ void otSysLedSet(uint8_t aLed, bool aOn) { switch (aLed) { case 1: nrf_gpio_pin_write(LED_1_PIN, (aOn == GPIO_LOGIC_HI)); break; case 2: nrf_gpio_pin_write(LED_2_PIN, (aOn == GPIO_LOGIC_HI)); break; case 3: nrf_gpio_pin_write(LED_3_PIN, (aOn == GPIO_LOGIC_HI)); break; case 4: nrf_gpio_pin_write(LED_4_PIN, (aOn == GPIO_LOGIC_HI)); break; } }
HÀNH ĐỘNG: Thêm 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 thông báo UDP đa điểm.
/** * @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ý thao tác nhấn nút.
Hàm đầu tiên khởi tạo bảng điều khiển cho một lần nhấn nút và hàm thứ hai gửi thông báo UDP đa điểm khi nhấn Nút 1.
/** * @brief Function to initialize the button. */ void otSysButtonInit(otSysButtonCallback aCallback) { nrfx_gpiote_in_config_t in_config = NRFX_GPIOTE_CONFIG_IN_SENSE_LOTOHI(true); in_config.pull = NRF_GPIO_PIN_PULLUP; ret_code_t err_code; err_code = nrfx_gpiote_in_init(BUTTON_PIN, &in_config, in_pin1_handler); APP_ERROR_CHECK(err_code); sButtonHandler = aCallback; sButtonPressed = false; nrfx_gpiote_in_event_enable(BUTTON_PIN, true); } void otSysButtonProcess(otInstance *aInstance) { if (sButtonPressed) { sButtonPressed = false; sButtonHandler(aInstance); } }
HÀNH ĐỘNG: Lưu và đóng tệp gpio.c
.
6. API: Phản ứng trước các thay đổi về vai trò của thiết bị
Trong ứng dụng của mình, 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 các vai trò sau: Trình dẫn đầu, Bộ định tuyến, Thiết bị cuối. Chúng ta có thể chỉ định các giá trị này cho đèn LED như sau:
- LED1 = Thủ lĩnh
- LED2 = Bộ định tuyến
- LED3 = Thiết bị cuối
Để bật chức năng này, ứng dụng cần biết 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 thực thể OpenThread cho phần đầu tiên và bản tóm tắt nền tảng GPIO cho phần thứ hai.
Mở tệp ./openthread/examples/apps/cli/main.c
trong trình chỉnh sửa văn bản mà bạn muốn dùng.
./openthread/examples/apps/cli/main.c
HÀNH ĐỘNG: Thêm tiêu đề bao gồm.
Trong phần bao gồm của tệp main.c
, hãy thêm các tệp tiêu đề API mà bạn cần cho tính năng thay đổi vai trò.
#include <openthread/instance.h> #include <openthread/thread.h> #include <openthread/thread_ftd.h>
HÀNH ĐỘNG: Thêm phần khai báo hàm xử lý cho thay đổi trạng thái thực thể OpenThread.
Thêm nội dung 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ượt đăng ký lệnh gọi lại 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
. Lệnh đă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 phương thức triển khai thay đổi trạng thái.
Trong main.c
, sau hàm main()
, hãy triển khai hàm handleNetifStateChanged
. Hàm này kiểm tra cờ OT_CHANGED_THREAD_ROLE
của thực thể OpenThread và nếu cờ này đã thay đổi, hãy bật/tắt đèn LED nếu 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 tính năng truyền tin nhiều điểm để bật đèn LED
Trong ứng dụng của mình, chúng ta cũng muốn gửi thông điệp UDP đến tất cả các thiết bị khác trong mạng khi nhấn nút Button1 trên một bảng. Để xác nhận đã nhận được thông báo, chúng ta sẽ bật/tắ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 kết nối UDP khi khởi động
- Có thể gửi thông báo UDP đến địa chỉ truyền tin đa địa phương 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 điệp 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 tiêu đề bao gồm.
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 cần cho tính năng UDP đa địa chỉ.
#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 linh hoạt.
HÀNH ĐỘNG: Thêm định nghĩa và hằng số:
Trong tệp main.c
, sau phần includes (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 nhiều điểm đến 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ả Thiết bị có luồng đầy đủ trong mạng. Hãy xem phần Multicast trên openthread.io để biết thêm thông tin về tính năng hỗ trợ truyền đa điểm trong OpenThread.
HÀNH ĐỘNG: Thêm nội dung khai báo hàm.
Trong tệp main.c
, sau phần khai báo 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ị ổ 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 chạy đè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 chân GPIO và GPIOTE, đồng thời đặ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 chạy khi ứng dụng khởi động. Nếu không có, thiết bị 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 phần 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 quá trình khởi chạy UDP.
Trong main.c
, hãy thêm phương thức triển khai hàm initUdp
sau hàm handleButtonInterrupt
mà bạn vừa thêm:
/** * Initialize UDP socket */ void initUdp(otInstance *aInstance) { otSockAddr listenSockAddr; memset(&sUdpSocket, 0, sizeof(sUdpSocket)); memset(&listenSockAddr, 0, sizeof(listenSockAddr)); listenSockAddr.mPort = UDP_PORT; otUdpOpen(aInstance, &sUdpSocket, handleUdpReceive, aInstance); otUdpBind(aInstance, &sUdpSocket, &listenSockAddr, OT_NETIF_THREAD); }
UDP_PORT
là cổng bạn đã xác định trước đó (1212). Hàm otUdpOpen
mở ổ cắm và đăng ký một hàm gọi lại (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
. Đối với các tuỳ chọn giao diện mạng khác, hãy tham khảo enum otNetifIdentifier
trong Tài liệu tham khảo API UDP.
HÀNH ĐỘNG: Triển khai tính năng nhắn tin qua 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 linh hoạt bằng cách chuyển đến khối exit
để giải phóng vùng đệm.
Xem 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 chạy UDP.
HÀNH ĐỘNG: Triển khai tính năng xử lý thông báo 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. Hàm này chỉ bật/tắt LED4.
/** * Function to handle UDP datagrams received on the listening socket */ void handleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo) { OT_UNUSED_VARIABLE(aContext); OT_UNUSED_VARIABLE(aMessage); OT_UNUSED_VARIABLE(aMessageInfo); otSysLedToggle(4); }
8. API: Định cấu hình mạng Thread
Để dễ minh hoạ, chúng ta muốn các thiết bị của mình bắt đầu Thread ngay lập tức và kết nối với nhau thành một mạng khi được bật nguồn. Để làm 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 mạng Thread đến một thiết bị.
Việc sử dụng cấu trúc này sẽ ghi đè các tuỳ chọn mặc định của mạng được tích hợp vào OpenThread, giúp ứng dụng của chúng ta an toàn hơn và chỉ giới hạn các nút Thread trong mạng của chúng ta ở 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 sử dụng.
./openthread/examples/apps/cli/main.c
HÀNH ĐỘNG: Thêm tiêu đề bao gồm.
Trong phần includes (bao gồm) ở đầ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 phần khai báo hàm để thiết lập cấu hình mạng.
Thêm nội dung 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 mạng Thread và ngăn xếp.
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 tham số mạng Thread mà chúng ta đang sử dụng cho ứng dụng này là:
- Channel = 15
- Mã số thẻ tín dụng = 0x2222
- Mã số định danh PAN mở rộng = C0DE1AB5C0DE1AB5
- Khoá mạng = 1234C0DE1AB51234C0DE1AB51234C0DE
- Tên mạng = OTCodelab
Ngoài ra, đây là nơi chúng ta giảm độ trễ lựa chọn bộ định tuyến để các thiết bị của chúng ta thay đổi vai trò nhanh hơn cho mục đích minh hoạ. Xin lưu ý rằng việc này chỉ được thực hiện nếu nút là FTD (Thiết bị luồng đầy đủ). Chúng ta sẽ tìm hiểu thêm về vấn đề này 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 được sửa đổi cho mục đích minh hoạ hoặc thử nghiệm. Không nên sử dụng các API này trong quá trình triển khai 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) cần thiết để Thiết bị cuối tự quảng bá chính nó với Bộ định tuyến. Giá trị mặc định của giá trị này là 120 theo Thông số kỹ thuật của 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 để nút Luồng thay đổi vai trò.
Lưu ý: Thiết bị MTD không trở thành bộ định tuyến và không hỗ trợ chức năng như otThreadSetRouterSelectionJitter
trong bản dựng MTD. Sau đó, chúng ta cần chỉ định tuỳ chọn CMake -DOT_MTD=OFF
, nếu không, chúng ta sẽ gặp lỗi bản dựng.
Bạn có thể xác nhận điều này bằng cách xem định nghĩa hàm otThreadSetRouterSelectionJitter
, nằm trong một lệnh của trình xử lý trước 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 cập nhật một số nội dung nhỏ cho 3 tệp CMake. Các tệp 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 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 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 NordicSemiconductor CMakeLists.txt
để 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 NordicSemiconductor 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 tệp 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 cập nhật tất cả mã, bạn đã sẵn sàng tạo và cài đặt ROM cho ứng dụng trên cả ba bo mạch phát triển Nordic nRF52840. Mỗi thiết bị sẽ hoạt động như một Thiết bị luồng đầy đủ (FTD).
Xây dựng OpenThread
Tạo tệp nhị phân OpenThread FTD cho nền tảng nRF52840.
$ cd ~/ot-nrf528xx $ ./script/build nrf52840 UART_trans -DOT_MTD=OFF -DOT_APP_RCP=OFF -DOT_RCP=OFF
Chuyển đến thư mục chứa tệp nhị phân CLI OpenThread FTD 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
Cài đặt ROM cho các bảng
Truyền tệp ot-cli-ftd.hex
vào từng bo mạch 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, sau đó cắm vào máy Linux. Thiết lập chính xác, LED5 đang bật.
Như trước, hãy ghi lại số sê-ri của bo mạch nRF52840:
Chuyển đến vị trí của Bộ công cụ dòng lệnh nRFx và cài đặt ROM tệp hex FTD CLI OpenThread vào bảng nRF52840 bằ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
LED5 sẽ tắt trong giây lát trong quá trình cài đặt ROM. 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 "Đẩy ROM vào bảng mạch" này cho hai bảng mạch còn lại. Mỗi bo mạch phải được kết nối với máy Linux theo cùng một cách và lệnh để cài đặt ROM cũng giống nhau, ngoại trừ số sê-ri của bo mạch. Hãy nhớ sử dụng số sê-ri duy nhất của mỗi bo mạch trong
nrfjprog
lệnh cài đặt ROM.
Nếu thành công, LED1, LED2 hoặc LED3 sẽ sáng trên mỗi bảng. Bạn thậm chí có thể thấy đèn LED sáng chuyển từ 3 sang 2 (hoặc 2 sang 1) ngay sau khi cài đặt ROM (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ả ba bo mạch nRF52840 đều được cấp nguồn và chạy ứng dụng OpenThread. Như đã trình bày chi tiết trước đó, ứng dụng này có hai tính năng chính.
Chỉ báo vai trò của thiết bị
Đèn LED sáng trên mỗi bảng phản ánh vai trò hiện tại của nút Luồng:
- LED1 = Thủ lĩnh
- LED2 = Bộ định tuyến
- LED3 = Thiết bị cuối
Khi vai trò thay đổi, đèn LED sáng cũng thay đổi. Bạn đã thấy những thay đổi này trên một hoặc hai bảng trong vòng 20 giây sau khi mỗi thiết bị khởi động.
UDP Multicast
Khi nhấn nút Button1 trên một 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 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 sẽ vẫn bật hoặc tắt cho mỗi bảng cho đến khi nhận được 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ị bạn đã cài đặt ROM 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 cho bộ định tuyến (REED). Điều này có nghĩa là chúng có thể hoạt động như một Trình đị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 Trình định tuyến.
Luồng có thể hỗ trợ tối đa 32 Trình định tuyến, nhưng cố gắng giữ số lượng Trình định tuyến trong khoảng từ 16 đến 23. Nếu một 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ị đó sẽ tự động tự nâng cấp thành Bộ định tuyến. Thay đổi này sẽ xảy ra vào một thời điểm ngẫu nhiên trong 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ình điều phối), là một Router (Bộ định tuyến) chịu trách nhiệm quản lý tập hợp các Router trong mạng Thread. Khi tất cả thiết bị đều bật, sau 20 giây, một trong số đó sẽ là Thiết bị đầu tiên (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 Trình điều khiển chính bị xoá khỏi mạng Thread, thì một Bộ định tuyến khác sẽ tự nâng cấp lên Trình điều khiển chính để đảm bảo mạng vẫn có Trình điều khiển chính.
Tắt Bảng xếp hạng (bảng có LED1 sáng) bằng công tắc Power (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ình dẫn đầu) sẽ bật. Thiết bị này hiện là Thiết bị đầu mối của mạng Thread.
Bật lại Bảng xếp hạng ban đầu. Thiết bị này sẽ tự động kết nối lại với mạng Thread dưới dạng Thiết bị cuối (LED3 sáng). Trong vòng 20 giây (Độ trễ lựa chọn bộ định tuyến), thiết bị này sẽ tự quảng bá thành Bộ định tuyến (LED2 sáng).
Đặt lại các bảng
Tắt cả 3 bảng, sau đó bật lại và quan sát đèn LED. Bảng đầu tiên được cấp nguồn sẽ bắt đầu ở vai trò Leader (LED1 sáng)—Trình định tuyến đầu tiên trong mạng Thread sẽ tự động trở thành Leader.
Ban đầu, hai bảng mạch còn lại kết nối với mạng dưới dạng Thiết bị cuối (LED3 sáng) nhưng sẽ tự chuyển đổi thành 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 của bạn không nhận đủ nguồn điện hoặc kết nối vô tuyến giữa các bảng bị yếu, thì mạng Thread có thể tách thành các phân vùng và bạn có thể có nhiều thiết bị hiển thị là Trình điều khiển.
Luồ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 Lãnh đạo.
14. Minh hoạ: Gửi UDP multicast
Nếu tiếp tục từ bài tập trước, LED4 sẽ không sáng trên bất kỳ thiết bị nào.
Chọn một bảng bất kỳ rồi nhấn Nút 1. LED4 trên tất cả các bảng khác trong mạng Thread đang chạy ứng dụng sẽ bật/tắt trạng thái của chúng. Nếu tiếp tục từ bài tập trước, thì các quyền này hiện đã bật.
Nhấn lại Button1 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 Nút1 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 Button1 trên một trong các bảng mà LED4 hiện đang bật. LED4 vẫn bật cho bảng đó nhưng 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 đã phân vùng và có nhiều Lãnh đạo trong số đó, thì kết quả của thông báo truyền nhiều điểm sẽ khác nhau giữa các bảng. Nếu bạn nhấn Nút 1 trên một bảng đã phân vùng (và do đó là thành viên duy nhất của mạng Luồng được phân vùng), thì LED4 trên các bảng khác sẽ không sáng lên để phản hồi. Nếu điều này xảy ra, hãy đặt lại các bảng điều khiển. Tốt nhất là chúng sẽ tái tạo 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 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 chân GPIO thay vì đèn LED trên bo mạch và kết nối đè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 tính năng truyền đa điểm để ping tất cả thiết bị khi nhấn nút, hãy sử dụng Router/Leader API (API Trình định tuyến/Trình dẫn đầu) để xác định vị trí và ping một thiết bị riêng lẻ
- Kết nối mạng lưới của bạn với Internet bằng cách sử dụng Bộ định tuyến biên OpenThread và truyền đa hướng các mạng đó từ bên ngoài mạng Thread để thắp sáng đèn LED
Tài liệu đọc thêm
Hãy xem openthread.io và GitHub để biết nhiều tài nguyên 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 (Hướng dẫn cơ bản về luồng) – một tài liệu tham khảo tuyệt vời về các khái niệm về luồng
Tài liệu tham khảo: