OpenThread API를 사용한 개발

이 Codelab 정보
schedule60분
subject최종 업데이트: 2025년 5월 5일
account_circle작성자: Jeff Bumgardner

1. 소개

26b7f4f6b3ea0700.png

Nest에서 출시한 OpenThreadThread® 네트워킹 프로토콜의 오픈소스 구현입니다. Nest는 커넥티드 홈용 제품 개발을 가속화하기 위해 Nest 제품에 사용되는 기술을 개발자가 광범위하게 사용할 수 있도록 OpenThread을 출시했습니다.

스레드 사양은 홈 애플리케이션을 위한 IPv6 기반의 안정적이고 안전하며 저전력 무선 기기 간 통신 프로토콜을 정의합니다. OpenThread은 IPv6, 6LoWPAN, MAC 보안이 적용된 IEEE 802.15.4, 메시 링크 설정, 메시 라우팅을 비롯한 모든 Thread 네트워킹 레이어를 구현합니다.

이 Codelab에서는 OpenThread API를 사용하여 Thread 네트워크를 시작하고, 기기 역할의 변경사항을 모니터링하고 이에 반응하며, UDP 메시지를 전송하고, 이러한 작업을 실제 하드웨어의 버튼 및 LED에 연결합니다.

2a6db2e258c32237.png

학습할 내용

  • Nordic nRF52840 개발 보드에서 버튼과 LED를 프로그래밍하는 방법
  • 일반적인 OpenThread API 및 otInstance 클래스를 사용하는 방법
  • OpenThread 상태 변경사항을 모니터링하고 이에 반응하는 방법
  • Thread 네트워크의 모든 기기에 UDP 메시지를 전송하는 방법
  • Makefile 수정 방법

필요한 항목

하드웨어:

  • Nordic Semiconductor nRF52840 개발 보드 3개
  • 보드를 연결하는 USB to Micro-USB 케이블 3개
  • USB 포트가 3개 이상 있는 Linux 머신

소프트웨어:

  • GNU 도구 모음
  • Nordic nRF5x 명령줄 도구
  • Segger J-Link 소프트웨어
  • OpenThread
  • Git

달리 명시되지 않는 한 이 Codelab의 콘텐츠는 크리에이티브 커먼즈 저작자 표시 3.0 라이선스에 따라 라이선스가 부여되며 코드 샘플은 Apache 2.0 라이선스에 따라 라이선스가 부여됩니다.

2. 시작하기

하드웨어 Codelab 완료

이 Codelab을 시작하기 전에 nRF52840 보드 및 OpenThread으로 Thread 네트워크 빌드 Codelab을 완료해야 합니다. 이 Codelab에서는 다음을 수행합니다.

  • 빌드 및 플래싱에 필요한 모든 소프트웨어에 관한 세부정보
  • OpenThread을 빌드하고 Nordic nRF52840 보드에 플래시하는 방법을 설명합니다.
  • Thread 네트워크의 기본을 보여줍니다.

이 Codelab에서는 OpenThread를 빌드하고 보드를 플래시하는 데 필요한 환경 설정은 자세히 설명하지 않습니다. 보드를 플래시하는 기본 안내만 제공합니다. 스레드 네트워크 빌드 Codelab을 이미 완료했다고 가정합니다.

Linux 시스템

이 Codelab은 i386 또는 x86 기반 Linux 시스템을 사용하여 모든 Thread 개발 보드를 플래시하도록 설계되었습니다. 모든 단계는 Ubuntu 14.04.5 LTS (Trusty Tahr)에서 테스트했습니다.

Nordic Semiconductor nRF52840 보드

이 Codelab에서는 nRF52840 PDK 보드 3개를 사용합니다.

a6693da3ce213856.png

소프트웨어 설치

OpenThread를 빌드하고 플래시하려면 SEGGER J-Link, nRF5x 명령줄 도구, ARM GNU 도구 모음, 다양한 Linux 패키지를 설치해야 합니다. 필요에 따라 스레드 네트워크 빌드 Codelab을 완료했다면 필요한 모든 항목이 이미 설치되어 있습니다. 그렇지 않은 경우 계속 진행하기 전에 해당 Codelab을 완료하여 OpenThread을 nRF52840 개발 보드에 빌드하고 플래시할 수 있는지 확인하세요.

3. 저장소 복제

OpenThread에는 이 Codelab의 시작점으로 사용할 수 있는 애플리케이션 코드 예시가 포함되어 있습니다.

OpenThread Nordic nRF528xx 예시 저장소를 클론하고 OpenThread을 빌드합니다.

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

4. OpenThread API 기본사항

OpenThread의 공개 API는 OpenThread 저장소의 ./openthread/include/openthread에 있습니다. 이러한 API는 애플리케이션에서 사용할 수 있도록 스레드 수준과 플랫폼 수준에서 다양한 OpenThread 기능에 액세스할 수 있도록 지원합니다.

  • OpenThread 인스턴스 정보 및 제어
  • IPv6, UDP, CoAP와 같은 애플리케이션 서비스
  • 커미셔너 및 참여자 역할과 함께 네트워크 사용자 인증 정보 관리
  • 보더 라우터 관리
  • 자녀 감독 및 신호 방해 감지와 같은 향상된 기능

모든 OpenThread API에 관한 참조 정보는 openthread.io/reference에서 확인할 수 있습니다.

API 사용

API를 사용하려면 애플리케이션 파일 중 하나에 API의 헤더 파일을 포함합니다. 그런 다음 원하는 함수를 호출합니다.

예를 들어 OpenThread에 포함된 CLI 예시 앱은 다음 API 헤더를 사용합니다.

./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>

OpenThread 인스턴스

otInstance 구조는 OpenThread API를 사용할 때 자주 사용하게 됩니다. 이 구조체는 초기화되면 OpenThread 라이브러리의 정적 인스턴스를 나타내며 사용자가 OpenThread API를 호출할 수 있도록 합니다.

예를 들어 OpenThread 인스턴스는 CLI 예시 앱의 main() 함수에서 초기화됩니다.

./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;
}

플랫폼별 함수

OpenThread에 포함된 예시 애플리케이션 중 하나에 플랫폼별 함수를 추가하려면 먼저 모든 함수에 otSys 네임스페이스를 사용하여 ./openthread/examples/platforms/openthread-system.h 헤더에서 함수를 선언합니다. 그런 다음 플랫폼별 소스 파일에 구현합니다. 이렇게 추상화하면 다른 예시 플랫폼에 동일한 함수 헤더를 사용할 수 있습니다.

예를 들어 nRF52840 버튼과 LED에 연결하는 데 사용할 GPIO 함수는 openthread-system.h에 선언해야 합니다.

선호하는 텍스트 편집기에서 ./openthread/examples/platforms/openthread-system.h 파일을 엽니다.

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

조치: 플랫폼별 GPIO 함수 선언을 추가합니다.

openthread/instance.h 헤더의 #include 뒤에 다음 함수 선언을 추가합니다.

/**
 * 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);

다음 단계에서 이를 구현합니다.

otSysButtonProcess 함수 선언은 otInstance를 사용합니다. 이렇게 하면 필요한 경우 버튼을 누르면 애플리케이션이 OpenThread 인스턴스에 관한 정보에 액세스할 수 있습니다. 이는 모두 애플리케이션의 요구사항에 따라 다릅니다. 함수 구현에 필요하지 않은 경우 OpenThread API의 OT_UNUSED_VARIABLE 매크로를 사용하여 일부 도구 모음에서 사용되지 않는 변수와 관련된 빌드 오류를 억제할 수 있습니다. 나중에 예를 살펴보겠습니다.

5. GPIO 플랫폼 추상화 구현

이전 단계에서는 GPIO에 사용할 수 있는 ./openthread/examples/platforms/openthread-system.h의 플랫폼별 함수 선언을 살펴봤습니다. nRF52840 개발 보드의 버튼과 LED에 액세스하려면 nRF52840 플랫폼에 이러한 함수를 구현해야 합니다. 이 코드에서는 다음과 같은 함수를 추가합니다.

  • GPIO 핀 및 모드 초기화
  • 핀의 전압 제어
  • GPIO 인터럽트 사용 설정 및 콜백 등록

./src/src 디렉터리에서 gpio.c라는 새 파일을 만듭니다. 이 새 파일에 다음 콘텐츠를 추가합니다.

./src/src/gpio.c (새 파일)

조치: 정의 추가

이러한 정의는 nRF52840 관련 값과 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

nRF52840 버튼 및 LED에 관한 자세한 내용은 Nordic Semiconductor Infocenter를 참고하세요.

조치: 헤더 포함 추가

다음으로 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"

조치: 버튼 1의 콜백 및 인터럽트 함수를 추가합니다.

다음으로 다음 코드를 추가합니다. in_pin1_handler 함수는 버튼 누르기 기능이 초기화될 때 등록되는 콜백입니다 (이 파일 뒷부분 참고).

in_pin1_handler에 전달된 변수가 함수에서 실제로 사용되지 않으므로 이 콜백에서 OT_UNUSED_VARIABLE 매크로를 사용하는 방식에 유의하세요.

/* 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;
}

조치: LED를 구성하는 함수를 추가합니다.

이 코드를 추가하여 초기화 중에 모든 LED의 모드와 상태를 구성합니다.

/**
 * @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);
}

조치: LED 모드를 설정하는 함수를 추가합니다.

이 함수는 기기의 역할이 변경될 때 사용됩니다.

/**
 * @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;
    }
}

조치: LED 모드를 전환하는 함수를 추가합니다.

이 함수는 기기가 멀티캐스트 UDP 메시지를 수신할 때 LED4를 전환하는 데 사용됩니다.

/**
 * @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;
    }
}

조치: 버튼 누르기를 초기화하고 처리하는 함수를 추가합니다.

첫 번째 함수는 버튼 누르기에 관한 보드를 초기화하고, 두 번째 함수는 버튼 1이 눌리면 멀티캐스트 UDP 메시지를 전송합니다.

/**
 * @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);
    }
}

조치: gpio.c 파일을 저장하고 닫습니다.

6. API: 기기 역할 변경에 반응

이 애플리케이션에서는 기기 역할에 따라 서로 다른 LED가 켜지도록 합니다. 리더, 라우터, 최종 기기 역할을 추적해 보겠습니다. 다음과 같이 LED에 할당할 수 있습니다.

  • LED1 = 리더
  • LED2 = 라우터
  • LED3 = 최종 기기

이 기능을 사용 설정하려면 애플리케이션이 기기 역할이 변경된 시점과 이에 대응하여 올바른 LED를 켜는 방법을 알아야 합니다. 첫 번째 부분에는 OpenThread 인스턴스를 사용하고 두 번째 부분에는 GPIO 플랫폼 추상화를 사용합니다.

선호하는 텍스트 편집기에서 ./openthread/examples/apps/cli/main.c 파일을 엽니다.

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

조치: 헤더 포함 추가

main.c 파일의 include 섹션에 역할 변경 기능에 필요한 API 헤더 파일을 추가합니다.

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

조치: OpenThread 인스턴스 상태 변경에 관한 핸들러 함수 선언을 추가합니다.

이 선언을 main.c에 추가합니다. 헤더가 포함된 후 #if 문 앞에 추가합니다. 이 함수는 기본 애플리케이션 뒤에 정의됩니다.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

조치: 상태 변경 핸들러 함수의 콜백 등록을 추가합니다.

main.c에서 otAppCliInit 호출 뒤의 main() 함수에 이 함수를 추가합니다. 이 콜백 등록은 OpenThread 인스턴스 상태가 변경될 때마다 handleNetifStateChange 함수를 호출하도록 OpenThread에 지시합니다.

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

조치: 상태 변경 구현을 추가합니다.

main.c에서 main() 함수 뒤에 handleNetifStateChanged 함수를 구현합니다. 이 함수는 OpenThread 인스턴스의 OT_CHANGED_THREAD_ROLE 플래그를 확인하고 변경된 경우 필요에 따라 LED를 켜거나 끕니다.

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: 멀티캐스트를 사용하여 LED 켜기

또한 애플리케이션에서는 한 보드에서 Button1이 눌리면 네트워크의 다른 모든 기기에 UDP 메시지를 전송합니다. 메시지 수신을 확인하기 위해 다른 보드의 LED4를 전환합니다.

이 기능을 사용 설정하려면 애플리케이션에서 다음을 실행해야 합니다.

  • 시작 시 UDP 연결 초기화
  • 메시 로컬 멀티캐스트 주소로 UDP 메시지를 보낼 수 있습니다.
  • 수신 UDP 메시지 처리
  • 수신 UDP 메시지에 대한 응답으로 LED4 전환

선호하는 텍스트 편집기에서 ./openthread/examples/apps/cli/main.c 파일을 엽니다.

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

조치: 헤더 포함 추가

main.c 파일 상단의 include 섹션에 멀티캐스트 UDP 기능에 필요한 API 헤더 파일을 추가합니다.

#include <string.h>

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

#include "utils/code_utils.h"

code_utils.h 헤더는 런타임 조건을 검사하고 오류를 적절하게 처리하는 otEXPECTotEXPECT_ACTION 매크로에 사용됩니다.

조치: 정의 및 상수 추가:

main.c 파일에서 include 섹션 뒤와 #if 문이 시작되기 전에 UDP 관련 상수와 정의가 추가됩니다.

#define UDP_PORT 1212

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

ff03::1는 메시 로컬 멀티캐스트 주소입니다. 이 주소로 전송된 메시지는 네트워크의 모든 전체 대화목록 기기로 전송됩니다. OpenThread의 멀티캐스트 지원에 관한 자세한 내용은 openthread.io의 멀티캐스트를 참고하세요.

조치: 함수 선언 추가

main.c 파일에서 otTaskletsSignalPending 정의 뒤와 main() 함수 앞에 UDP용 함수와 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;

조치: GPIO LED 및 버튼을 초기화하는 호출을 추가합니다.

main.c에서 otSetStateChangedCallback 호출 뒤에 main() 함수에 이러한 함수 호출을 추가합니다. 이러한 함수는 GPIO 및 GPIOTE 핀을 초기화하고 버튼 푸시 이벤트를 처리하도록 버튼 핸들러를 설정합니다.

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

조치: UDP 초기화 호출을 추가합니다.

main.c에서 방금 추가한 otSysButtonInit 호출 뒤에 main() 함수에 다음 함수를 추가합니다.

initUdp(instance);

이 호출을 통해 애플리케이션 시작 시 UDP 소켓이 초기화됩니다. 이 없이는 기기가 UDP 메시지를 보내거나 받을 수 없습니다.

조치: GPIO 버튼 이벤트를 처리하는 호출을 추가합니다.

main.c에서 while 루프의 otSysProcessDrivers 호출 뒤에 있는 main() 함수에 이 함수 호출을 추가합니다. gpio.c에 선언된 이 함수는 버튼이 눌렸는지 확인하고 눌린 경우 위 단계에서 설정된 핸들러 (handleButtonInterrupt)를 호출합니다.

otSysButtonProcess(instance);

조치: 버튼 인터럽트 핸들러 구현

main.c에서 이전 단계에서 추가한 handleNetifStateChanged 함수 뒤에 handleButtonInterrupt 함수의 구현을 추가합니다.

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

조치: UDP 초기화를 구현합니다.

main.c에서 방금 추가한 handleButtonInterrupt 함수 뒤에 initUdp 함수의 구현을 추가합니다.

/**
 * 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는 이전에 정의한 포트 (1212)입니다. otUdpOpen 함수는 소켓을 열고 UDP 메시지가 수신될 때의 콜백 함수 (handleUdpReceive)를 등록합니다. otUdpBindOT_NETIF_THREAD를 전달하여 소켓을 스레드 네트워크 인터페이스에 바인딩합니다. 다른 네트워크 인터페이스 옵션은 UDP API 참조otNetifIdentifier 열거형을 참고하세요.

조치: UDP 메시지 구현

main.c에서 방금 추가한 initUdp 함수 뒤에 sendUdp 함수의 구현을 추가합니다.

/**
 * 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);
    }
}

otEXPECTotEXPECT_ACTION 매크로를 확인합니다. 이렇게 하면 UDP 메시지가 유효하고 버퍼에 올바르게 할당되었는지 확인할 수 있으며, 그렇지 않은 경우 함수는 exit 블록으로 이동하여 버퍼를 해제하는 방식으로 오류를 적절하게 처리합니다.

UDP를 초기화하는 데 사용되는 함수에 관한 자세한 내용은 openthread.io의 IPv6UDP 참조를 참고하세요.

조치: UDP 메시지 처리를 구현합니다.

main.c에서 방금 추가한 sendUdp 함수 뒤에 handleUdpReceive 함수의 구현을 추가합니다. 이 함수는 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: 스레드 네트워크 구성

설명의 편의를 위해 기기가 켜지면 즉시 Thread를 시작하고 네트워크에 참여하도록 하겠습니다. 이를 위해 otOperationalDataset 구조를 사용합니다. 이 구조체는 Thread 네트워크 사용자 인증 정보를 기기로 전송하는 데 필요한 모든 매개변수를 보유합니다.

이 구조를 사용하면 OpenThread에 내장된 네트워크 기본값이 재정의되어 애플리케이션의 보안이 강화되고 네트워크의 Thread 노드가 애플리케이션을 실행하는 노드로만 제한됩니다.

선호하는 텍스트 편집기에서 ./openthread/examples/apps/cli/main.c 파일을 다시 엽니다.

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

조치: 헤더 포함 추가

main.c 파일 상단의 include 섹션 내에 스레드 네트워크를 구성하는 데 필요한 API 헤더 파일을 추가합니다.

#include <openthread/dataset_ftd.h>

조치: 네트워크 구성 설정을 위한 함수 선언을 추가합니다.

이 선언을 main.c에 추가합니다. 헤더가 포함된 후 #if 문 앞에 추가합니다. 이 함수는 기본 애플리케이션 함수 뒤에 정의됩니다.

static void setNetworkConfiguration(otInstance *aInstance);

조치: 네트워크 구성 호출을 추가합니다.

main.c에서 otSetStateChangedCallback 호출 뒤에 main() 함수에 이 함수 호출을 추가합니다. 이 함수는 스레드 네트워크 데이터 세트를 구성합니다.

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

조치: 스레드 네트워크 인터페이스와 스택을 사용 설정하는 호출을 추가합니다.

main.c에서 otSysButtonInit 호출 뒤에 main() 함수에 이러한 함수 호출을 추가합니다.

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

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

조치: 스레드 네트워크 구성을 구현합니다.

main.c에서 main() 함수 뒤에 setNetworkConfiguration 함수의 구현을 추가합니다.

/**
 * 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);
}

함수에 설명된 대로 이 애플리케이션에 사용되는 스레드 네트워크 매개변수는 다음과 같습니다.

  • 채널 = 15
  • PAN ID = 0x2222
  • 확장 PAN ID = C0DE1AB5C0DE1AB5
  • 네트워크 키 = 1234C0DE1AB51234C0DE1AB51234C0DE
  • 네트워크 이름 = OTCodelab

또한 여기에서 라우터 선택 지터를 줄여 데모 목적으로 기기가 더 빠르게 역할을 변경합니다. 이는 노드가 FTD (전체 스레드 기기)인 경우에만 실행됩니다. 다음 단계에서 자세히 알아보세요.

9. API: 제한된 함수

일부 OpenThread API는 데모 또는 테스트 목적으로만 수정해야 하는 설정을 수정합니다. 이러한 API는 OpenThread를 사용하는 애플리케이션의 프로덕션 배포에서는 사용하면 안 됩니다.

예를 들어 otThreadSetRouterSelectionJitter 함수는 최종 기기가 라우터로 승격되는 데 걸리는 시간 (초)을 조정합니다. 이 값의 기본값은 스레드 사양에 따라 120입니다. 이 Codelab에서는 편의상 20으로 변경하여 스레드 노드가 역할을 변경할 때까지 오래 기다리지 않아도 됩니다.

참고: MTD 기기는 라우터가 되지 않으며 otThreadSetRouterSelectionJitter와 같은 함수 지원은 MTD 빌드에 포함되지 않습니다. 나중에 CMake 옵션 -DOT_MTD=OFF를 지정해야 합니다. 그러지 않으면 빌드가 실패합니다.

OPENTHREAD_FTD의 프리프로세서 지시문에 포함된 otThreadSetRouterSelectionJitter 함수 정의에서 이를 확인할 수 있습니다.

./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. CMake 업데이트

애플리케이션을 빌드하기 전에 세 개의 CMake 파일을 약간 업데이트해야 합니다. 이는 빌드 시스템에서 애플리케이션을 컴파일하고 연결하는 데 사용됩니다.

./third_party/NordicSemiconductor/CMakeLists.txt

이제 GPIO 함수가 애플리케이션에 정의되도록 NordicSemiconductor CMakeLists.txt에 플래그를 추가합니다.

조치: CMakeLists.txt 파일에 플래그를 추가합니다.

선호하는 텍스트 편집기에서 ./third_party/NordicSemiconductor/CMakeLists.txt을 열고 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

./src/CMakeLists.txt 파일을 수정하여 새 gpio.c 소스 파일을 추가합니다.

조치: gpio 소스를 ./src/CMakeLists.txt 파일에 추가합니다.

선호하는 텍스트 편집기에서 ./src/CMakeLists.txt를 열고 NRF_COMM_SOURCES 섹션에 파일을 추가합니다.

...

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

...

./third_party/NordicSemiconductor/CMakeLists.txt

마지막으로 nrfx_gpiote.c 드라이버 파일을 NordicSemiconductor CMakeLists.txt 파일에 추가하여 Nordic 드라이버의 라이브러리 빌드에 포함되도록 합니다.

조치: NordicSemiconductor CMakeLists.txt 파일에 gpio 드라이버를 추가합니다.

선호하는 텍스트 편집기에서 ./third_party/NordicSemiconductor/CMakeLists.txt를 열고 COMMON_SOURCES 섹션에 파일을 추가합니다.

...

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

11. 기기 설정

모든 코드 업데이트를 완료했으므로 이제 애플리케이션을 빌드하고 세 개의 Nordic nRF52840 개발 보드에 모두 플래시할 수 있습니다. 각 기기는 전체 스레드 기기 (FTD)로 작동합니다.

OpenThread 빌드

nRF52840 플랫폼용 OpenThread FTD 바이너리를 빌드합니다.

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

OpenThread FTD CLI 바이너리가 있는 디렉터리로 이동하여 ARM 임베디드 도구 모음을 사용하여 16진수 형식으로 변환합니다.

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

보드 플래시

ot-cli-ftd.hex 파일을 각 nRF52840 보드에 플래시합니다.

USB 케이블을 nRF52840 보드의 외부 전원 핀 옆에 있는 마이크로 USB 디버그 포트에 연결한 다음 Linux 머신에 연결합니다. 올바르게 설정하면 LED5가 켜집니다.

20a3b4b480356447.png

이전과 마찬가지로 nRF52840 보드의 일련번호를 확인합니다.

c00d519ebec7e5f0.jpeg

nRFx 명령줄 도구의 위치로 이동하여 보드의 일련번호를 사용하여 OpenThread CLI FTD 16진수 파일을 nRF52840 보드에 플래시합니다.

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

깜박이는 동안 LED5가 잠시 꺼집니다. 성공하면 다음과 같은 출력이 생성됩니다.

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.

다른 두 보드에 대해 이 '보드 플래시' 단계를 반복합니다. 각 보드는 동일한 방식으로 Linux 머신에 연결되어야 하며, 보드의 일련번호를 제외하고 플래시 명령은 동일합니다. 다음에서 각 보드의 고유한 일련번호를 사용해야 합니다.

nrfjprog 플래시 명령어

성공하면 각 보드에 LED1, LED2 또는 LED3이 켜집니다. 플래시가 곧바로 3에서 2 (또는 2에서 1)로 전환되는 LED가 표시될 수도 있습니다 (기기 역할 변경 기능).

12. 애플리케이션 기능

이제 세 개의 nRF52840 보드가 모두 전원을 공급받고 OpenThread 애플리케이션을 실행하고 있습니다. 앞서 설명한 대로 이 애플리케이션에는 두 가지 기본 기능이 있습니다.

기기 역할 표시기

각 보드의 켜진 LED는 스레드 노드의 현재 역할을 나타냅니다.

  • LED1 = 리더
  • LED2 = 라우터
  • LED3 = 최종 기기

역할이 변경되면 LED가 켜지는 방식도 달라집니다. 각 기기의 전원이 켜진 후 20초 이내에 보드 1~2개에서 이러한 변경사항을 확인할 수 있습니다.

UDP 멀티캐스트

보드에서 Button1을 누르면 Thread 네트워크의 다른 모든 노드가 포함된 메시 로컬 멀티캐스트 주소로 UDP 메시지가 전송됩니다. 이 메시지를 수신하면 다른 모든 보드의 LED4가 켜지거나 꺼집니다. LED4는 다른 UDP 메시지를 수신할 때까지 각 보드에서 켜지거나 꺼진 상태로 유지됩니다.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. 데모: 기기 역할 변경사항 관찰

플래시한 기기는 라우터 사용 가능 최종 기기 (REED)라고 하는 특정 종류의 전체 스레드 기기 (FTD)입니다. 즉, 라우터 또는 최종 기기로 작동할 수 있으며 최종 기기에서 라우터로 승격할 수 있습니다.

스레드는 최대 32개의 라우터를 지원할 수 있지만 라우터 수를 16~23개로 유지하려고 시도합니다. REED가 최종 기기로 연결되고 라우터 수가 16개 미만이면 REED가 자동으로 라우터로 승격됩니다. 이 변경사항은 애플리케이션에서 otThreadSetRouterSelectionJitter 값을 설정한 초 수 (20초) 내에 임의의 시간에 발생해야 합니다.

모든 스레드 네트워크에는 스레드 네트워크의 라우터 세트를 관리하는 라우터인 리더도 있습니다. 모든 기기를 켠 후 20초 후에 기기 중 하나가 리더 (LED1 켜짐)이고 나머지 두 기기가 라우터 (LED2 켜짐)가 됩니다.

4e1e885861a66570.png

리더 삭제

리더가 Thread 네트워크에서 삭제되면 다른 라우터가 리더로 승격되어 네트워크에 리더가 계속 유지되도록 합니다.

전원 스위치를 사용하여 리더보드 (LED1이 켜진 보드)를 끕니다. 20초 정도 기다립니다. 나머지 두 보드 중 하나에서 LED2 (라우터)가 꺼지고 LED1 (리더)이 켜집니다. 이제 이 기기가 스레드 네트워크의 리더입니다.

4c57c87adb40e0e3.png

원래 리더보드를 다시 사용 설정합니다. 그러면 기기가 엔드 기기로 Thread 네트워크에 자동으로 다시 연결됩니다 (LED3이 켜짐). 20초 (라우터 선택 지터) 이내에 라우터로 승격됩니다 (LED2가 켜짐).

5f40afca2dcc4b5b.png

보드 재설정

세 개의 보드를 모두 껐다가 다시 켜고 LED를 관찰합니다. 전원이 켜진 첫 번째 보드는 리더 역할로 시작해야 합니다 (LED1이 켜짐). 스레드 네트워크의 첫 번째 라우터는 자동으로 리더가 됩니다.

나머지 두 보드는 처음에는 네트워크에 최종 기기로 연결되지만 (LED3가 켜짐) 20초 이내에 라우터로 승격되어야 합니다 (LED2가 켜짐).

네트워크 파티션

보드에 전원이 충분하지 않거나 보드 간의 무선 연결이 약한 경우 스레드 네트워크가 파티션으로 분할될 수 있으며 두 개 이상의 기기가 리더로 표시될 수 있습니다.

스레드는 자체 복구되므로 파티션은 결국 리더가 하나인 단일 파티션으로 다시 병합됩니다.

14. 데모: UDP 멀티캐스트 전송

이전 연습에서 이어서 진행하는 경우 기기에서 LED4가 켜져서는 안 됩니다.

보드를 선택하고 Button1을 누릅니다. 애플리케이션을 실행하는 Thread 네트워크의 다른 모든 보드의 LED4가 상태를 전환해야 합니다. 이전 연습에서 이어서 진행하는 경우 이제 켜져 있어야 합니다.

f186a2618fdbe3fd.png

동일한 보드의 Button1을 다시 누릅니다. 다른 모든 보드의 LED4가 다시 전환됩니다.

다른 보드에서 Button1을 누르고 다른 보드에서 LED4가 전환되는 방식을 관찰합니다. LED4가 현재 켜져 있는 보드 중 하나에서 Button1을 누릅니다. LED4는 해당 보드에서 계속 켜져 있지만 다른 보드에서는 전환됩니다.

f5865ccb8ab7aa34.png

네트워크 파티션

보드가 파티션된 상태이고 그중 리더가 두 명 이상인 경우 보드 간에 멀티캐스트 메시지의 결과가 다릅니다. 파티션이 생성되어 파티션된 Thread 네트워크의 유일한 구성원인 보드에서 Button1을 누르면 다른 보드의 LED4가 응답으로 켜지지 않습니다. 이 경우 보드를 재설정합니다. 그러면 단일 스레드 네트워크가 다시 형성되고 UDP 메시지가 올바르게 작동합니다.

15. 축하합니다.

OpenThread API를 사용하는 애플리케이션을 만들었습니다.

이제 다음 사항을 알게 되었습니다.

  • Nordic nRF52840 개발 보드에서 버튼과 LED를 프로그래밍하는 방법
  • 일반적인 OpenThread API 및 otInstance 클래스를 사용하는 방법
  • OpenThread 상태 변경사항을 모니터링하고 이에 반응하는 방법
  • Thread 네트워크의 모든 기기에 UDP 메시지를 전송하는 방법
  • Makefile 수정 방법

다음 단계

이 Codelab을 기반으로 다음 연습을 해 보세요.

  • 온보드 LED 대신 GPIO 핀을 사용하도록 GPIO 모듈을 수정하고 라우터 역할에 따라 색상이 변경되는 외부 RGB LED를 연결합니다.
  • 다른 예시 플랫폼에 GPIO 지원 추가
  • 버튼 누르기에 대한 멀티캐스트를 사용하여 모든 기기에 핑을 보내는 대신 Router/Leader API를 사용하여 개별 기기를 찾아 핑합니다.
  • OpenThread 보더 라우터를 사용하여 메시 네트워크를 인터넷에 연결하고 스레드 네트워크 외부에서 멀티캐스트하여 LED를 켭니다.

추가 자료

openthread.ioGitHub에서 다음과 같은 다양한 OpenThread 리소스를 확인하세요.

참조: