본문 바로가기

ESPRESSIF/ESP32-C6

[ESP32C6 xBee] Zigbee 테스트 - Switch Coordinator Device 제작 및 코드분석

ESP32C6 Zigbee 장치를 테스트 하기 위해 Gateway 에 해당하는 Coordinator 장치가 필요한데 HA_on_off_switch 예제로 테스트 하면 된다.

 

ESP32C6 OnOff light EndDevice 를 제어 할 수 있는 코디네이터 장치를 제작해 보자.

 

 

Zigbee EndDevice 코드의 메인 함수

프로그램이 실행되면 Zigbee 관련 Task esp_zb_task() 함수를 수행한다.

주요 코드의 뼈대만 되면 아래와 같은 구조로 심플하다.

void app_main(void)
{
    /* Zigbee 플랫폼 설정 (라디오, 호스트 기본값 사용) */
    esp_zb_platform_config_t config = {
        .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(),  // 기본 Zigbee 라디오 설정
        .host_config  = ESP_ZB_DEFAULT_HOST_CONFIG(),   // 기본 Zigbee 호스트 설정
    };

    /* NVS 플래시 초기화 (Zigbee 네트워크 정보 저장에 필요) */
    ESP_ERROR_CHECK(nvs_flash_init());

    /* Zigbee 플랫폼 구성 적용 */
    ESP_ERROR_CHECK(esp_zb_platform_config(&config));

    /* FreeRTOS 태스크 생성 → Zigbee 스택 실행 */
    xTaskCreate(esp_zb_task, "Zigbee_main", 4096, NULL, 5, NULL);
}

//Zigbee 메인 태스크
static void esp_zb_task(void *pvParameters)
{
}

//Zigbee 애플리케이션 시그널 핸들러 (Coordinator)
void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct)
{
}

//새로운 디바이스를 찾았을 때 호출되는 콜백
static void user_find_cb(esp_zb_zdp_status_t zdo_status,
                         uint16_t addr, uint8_t endpoint, void *user_ctx)
{
}

//바인딩 요청(bind request) 결과 콜백
static void bind_cb(esp_zb_zdp_status_t zdo_status, void *user_ctx)
{
}

//버튼 입력 이벤트를 처리하여 해당 기능 실행(Light 장치로 Zigbee 데이터 전송)
static void zb_buttons_handler(switch_func_pair_t *button_func_pair)
{
}

 

 

 

Zigbee 실행  메인 태스크( esp_zb_task )

Zigbee 스택을 초기화 하고 실행하는 함수로 ZCD(Zigbee Coordinator Device) 로 설정하고 ZigBee Device configuration 구조체를 초기화 하고 Endpoint를 등록한다.

#define ESP_ZB_ZC_CONFIG()                                                              \
    {                                                                                   \
        .esp_zb_role = ESP_ZB_DEVICE_TYPE_COORDINATOR,                                  \
        .install_code_policy = INSTALLCODE_POLICY_ENABLE,                               \
        .nwk_cfg.zczr_cfg = {                                                           \
            .max_children = MAX_CHILDREN,                                               \
        },                                                                              \
    }

 

 

장치 설정 후 Zigbee 스택 실행

//Zigbee 메인 태스크
static void esp_zb_task(void *pvParameters)
{
    /* Zigbee 스택 초기화 */
    esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZC_CONFIG();
    esp_zb_init(&zb_nwk_cfg);

    // On/Off Switch 엔드포인트 생성
    esp_zb_on_off_switch_cfg_t switch_cfg = ESP_ZB_DEFAULT_ON_OFF_SWITCH_CONFIG();
    esp_zb_ep_list_t *esp_zb_on_off_switch_ep =
        esp_zb_on_off_switch_ep_create(HA_ONOFF_SWITCH_ENDPOINT, &switch_cfg);

    // 제조사 / 모델 정보 추가
    zcl_basic_manufacturer_info_t info = {
        .manufacturer_name = ESP_MANUFACTURER_NAME,
        .model_identifier  = ESP_MODEL_IDENTIFIER,
    };
    esp_zcl_utility_add_ep_basic_manufacturer_info(esp_zb_on_off_switch_ep,
                                                   HA_ONOFF_SWITCH_ENDPOINT,
                                                   &info);

    // 디바이스 등록
    esp_zb_device_register(esp_zb_on_off_switch_ep);

    // 네트워크 채널 설정
    esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK);

    // Zigbee 스택 실행 시작
    ESP_ERROR_CHECK(esp_zb_start(false));

    // Zigbee 스택 메인 루프 진입
    esp_zb_stack_main_loop();
}

 

 

 

Zigbee 애플리케이션 시그널 핸들러

Zigbee 스택 내부에서 발생하는 이벤트(시그널)를 처리하는 콜백 함수 이고 초기화, 기기 첫 시작, 재부팅, 네트워크 조인/스티어링 결과 등을 구분하여 동작 한다.

 

Zigbee 실행 순서

  • 초기화 (SKIP_STARTUP) → BDB 초기화 시작.
  • 첫 시작/재부팅
  • 공장 초기화 상태면 → 네트워크 형성(formation)
  • 아니면 → 기존 네트워크 열고 permit join.
  • FORMATION → PAN ID, 채널, Extended PAN ID 로그 출력 후 steering 시작.
  • STEERING → 네트워크 열려 있음.
  • DEVICE ANNCE → 새 장치 가입/재가입 시 → 매치 디스크립터 요청(컬러 디머 라이트 탐색).
  • PERMIT JOIN STATUS → 네트워크가 열려 있는지/닫혔는지 출력.

다른 메시지들은 대부분의 프로젝트에서 거의 동일하게 사용되고 DEVICE ANNCE 신호는 어플리케이셔에 따라 수정해 주어야 하는 부분이므로 눈여겨 봐 두는것이 좋을것 같다.

//Zigbee 애플리케이션 시그널 핸들러 (Coordinator)
void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct)
{
    uint32_t *p_sg_p = signal_struct->p_app_signal;
    esp_err_t err_status = signal_struct->esp_err_status;
    esp_zb_app_signal_type_t sig_type = *p_sg_p;
    esp_zb_zdo_signal_device_annce_params_t *dev_annce_params = NULL;

    switch (sig_type) {
    case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP:
        // 스택 초기화 단계
        ESP_LOGI(TAG, "Initialize Zigbee stack");
        esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_INITIALIZATION);
        break;

    case ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START:
    case ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT:
        // 첫 시작 또는 리부트 시 동작
        if (err_status == ESP_OK) {
            ESP_LOGI(TAG, "Deferred driver initialization %s",
                     deferred_driver_init() ? "failed" : "successful");
            ESP_LOGI(TAG, "Device started up in%s factory-reset mode",
                     esp_zb_bdb_is_factory_new() ? "" : " non");

            if (esp_zb_bdb_is_factory_new()) {
                // 공장 초기화 상태 → 네트워크 포메이션 시작
                ESP_LOGI(TAG, "Start network formation");
                esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_FORMATION);
            } else {
                // 기존 네트워크 상태 → 네트워크 오픈
                esp_zb_bdb_open_network(180); // 180초 동안 permit join
                ESP_LOGI(TAG, "Device rebooted");
            }
        } else {
            // 실패 시 재시도 예약
            ESP_LOGW(TAG, "%s failed with status: %s, retrying",
                     esp_zb_zdo_signal_to_string(sig_type),
                     esp_err_to_name(err_status));
            esp_zb_scheduler_alarm((esp_zb_callback_t)bdb_start_top_level_commissioning_cb,
                                   ESP_ZB_BDB_MODE_INITIALIZATION, 1000);
        }
        break;

    case ESP_ZB_BDB_SIGNAL_FORMATION:
        // 네트워크 포메이션 결과
        if (err_status == ESP_OK) {
            esp_zb_ieee_addr_t extended_pan_id;
            esp_zb_get_extended_pan_id(extended_pan_id);
            ESP_LOGI(TAG, "Formed network successfully (Extended PAN ID: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x, "
                           "PAN ID: 0x%04hx, Channel:%d, Short Address: 0x%04hx)",
                     extended_pan_id[7], extended_pan_id[6], extended_pan_id[5], extended_pan_id[4],
                     extended_pan_id[3], extended_pan_id[2], extended_pan_id[1], extended_pan_id[0],
                     esp_zb_get_pan_id(), esp_zb_get_current_channel(), esp_zb_get_short_address());

            // 네트워크 포메이션 완료 후 네트워크 스티어링 시작
            esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING);
        } else {
            // 실패 시 재시도 예약
            ESP_LOGI(TAG, "Restart network formation (status: %s)",
                     esp_err_to_name(err_status));
            esp_zb_scheduler_alarm((esp_zb_callback_t)bdb_start_top_level_commissioning_cb,
                                   ESP_ZB_BDB_MODE_NETWORK_FORMATION, 1000);
        }
        break;

    case ESP_ZB_BDB_SIGNAL_STEERING:
        if (err_status == ESP_OK) {
            ESP_LOGI(TAG, "Network steering started");
        }
        break;

    case ESP_ZB_ZDO_SIGNAL_DEVICE_ANNCE:
        // 새로운 장치가 조인/재조인할 때 호출됨
        dev_annce_params = (esp_zb_zdo_signal_device_annce_params_t *)esp_zb_app_signal_get_params(p_sg_p);
        ESP_LOGI(TAG, "New device commissioned or rejoined (short: 0x%04hx)",
                 dev_annce_params->device_short_addr);

        // On/Off Light 검색 요청
        esp_zb_zdo_match_desc_req_param_t  cmd_req;
        cmd_req.dst_nwk_addr    = dev_annce_params->device_short_addr;
        cmd_req.addr_of_interest = dev_annce_params->device_short_addr;
        esp_zb_zdo_find_on_off_light(&cmd_req, user_find_cb, NULL);
        break;

    case ESP_ZB_NWK_SIGNAL_PERMIT_JOIN_STATUS:
        // 네트워크 permit join 상태 변화
        if (err_status == ESP_OK) {
            if (*(uint8_t *)esp_zb_app_signal_get_params(p_sg_p)) {
                ESP_LOGI(TAG, "Network(0x%04hx) is open for %d seconds",
                         esp_zb_get_pan_id(),
                         *(uint8_t *)esp_zb_app_signal_get_params(p_sg_p));
            } else {
                ESP_LOGW(TAG, "Network(0x%04hx) closed, devices joining not allowed.",
                         esp_zb_get_pan_id());
            }
        }
        break;

    default:
        // 기타 알 수 없는 이벤트 로그 출력
        ESP_LOGI(TAG, "ZDO signal: %s (0x%x), status: %s",
                 esp_zb_zdo_signal_to_string(sig_type), sig_type,
                 esp_err_to_name(err_status));
        break;
    }
}

 

 

 

새로운 장치를 찾았을때 콜백함수 user_find_cb

esp_zb_app_signal_handler 에서 Zigbee 장치를 찾으면 ESP_ZB_ZDO_SIGNAL_DEVICE_ANNCE 이벤트가 발생하고 그때 등록해둔 찾이 찾는 콜백 함수 user_find_cb() 가 수행된다.

이때 해당 장치의 short_addr, endpoint, ieee_addr를 알아내고, Color Control / Level Control 클러스터에 대해 바인딩 요청을 보내서 바인딩 할수 있다.
esp_zb_zdo_device_bind_req() 함수 실행

/**
 * @brief 새로운 디바이스를 찾았을 때 호출되는 콜백
 *        (예: On/Off Light 장치 발견 시)
 */
static void user_find_cb(esp_zb_zdp_status_t zdo_status,
                         uint16_t addr, uint8_t endpoint, void *user_ctx)
{
    if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) {
        ESP_LOGI(TAG, "Found light");

        // 바인딩 요청을 위한 구조체
        esp_zb_zdo_bind_req_param_t bind_req;

        // light 장치 정보 동적 할당
        light_bulb_device_params_t *light =
            (light_bulb_device_params_t *)malloc(sizeof(light_bulb_device_params_t));
        light->endpoint   = endpoint;
        light->short_addr = addr;

        // 단축 주소로 IEEE long address 조회
        esp_zb_ieee_address_by_short(light->short_addr, light->ieee_addr);

        // 로컬(스위치)의 long address 가져오기
        esp_zb_get_long_address(bind_req.src_address);

        // 소스(스위치) endpoint 지정
        bind_req.src_endp   = HA_ONOFF_SWITCH_ENDPOINT;

        // ON/OFF 클러스터 ID 지정
        bind_req.cluster_id = ESP_ZB_ZCL_CLUSTER_ID_ON_OFF;

        // 대상 주소는 long address 모드로 지정
        bind_req.dst_addr_mode = ESP_ZB_ZDO_BIND_DST_ADDR_MODE_64_BIT_EXTENDED;
        memcpy(bind_req.dst_address_u.addr_long,
               light->ieee_addr, sizeof(esp_zb_ieee_addr_t));

        // 대상 endpoint는 발견한 light의 endpoint 사용
        bind_req.dst_endp   = endpoint;

        // 바인딩 요청을 self에게 전송
        bind_req.req_dst_addr = esp_zb_get_short_address();

        ESP_LOGI(TAG, "Try to bind On/Off");
        esp_zb_zdo_device_bind_req(&bind_req, bind_cb, (void *)light);
    }
}

 

 

 

바인딩 요청 결과 콜백함수 user_find_cb

바인딩 요청 전송후(user_find_cb) 요청 완료되면 bind_cb()가 호출됨.

바인딩 성공 시 로그 출력. user_ctx로 전달된 light 장치 정보 해제.

bdb_start_top_level_commissioning_cb()

BDB 커미셔닝 모드 시작을 보장하는 헬퍼 콜백.

/**
 * @brief 바인딩 요청(bind request) 결과 콜백
 * @param zdo_status ZDO(Zigbee Device Object) 응답 상태
 * @param user_ctx   바인딩 시 함께 전달된 사용자 컨텍스트 (light 정보)
 */
static void bind_cb(esp_zb_zdp_status_t zdo_status, void *user_ctx)
{
    if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) {
        ESP_LOGI(TAG, "Bound successfully!");
        if (user_ctx) {
            // user_ctx로 전달받은 light 정보 출력 후 메모리 해제
            light_bulb_device_params_t *light = (light_bulb_device_params_t *)user_ctx;
            ESP_LOGI(TAG, "The light originating from address(0x%x) on endpoint(%d)",
                     light->short_addr, light->endpoint);
            free(light);
        }
    }
}

 


이렇게 해서 장치를 찾고 바인딩을 성공하면 이후 장치에 데이터를 보내거나 수신 할 수 있다.

light End Device는 수신만 하는 장치 이므로 간단한데 송신하는 장치의 경우 각 해당 장치에 대한 Arrtibute를 등록하고 수신하는 핸들러를 추가 해주어야 한다.

 

우선 End Device로 데이터를 보내는 코드를 보자
스위치 누를때 수행되는 콜백 함수에서 Zigbee 데이터를 전송하면 되는데.. ESP32 Zigbee SDK에서 각 장치별 전송 함수들을 다 작성 해 둔 esp_zb_zcl_on_off_cmd_req() 함수로 전송하면 된다.

/**
 * @brief 버튼 입력 이벤트를 처리하여 해당 기능 실행
 *
 * @param button_func_pair 버튼과 기능 매핑 구조체 (버튼이 눌렸을 때 실행할 기능 정보 포함)
 */
static void zb_buttons_handler(switch_func_pair_t *button_func_pair)
{
    // 버튼이 매핑된 기능이 "ON/OFF 토글"인지 확인
    if (button_func_pair->func == SWITCH_ONOFF_TOGGLE_CONTROL) {
        /* On/Off 토글 명령을 구성하여 전송 */
        esp_zb_zcl_on_off_cmd_t cmd_req;

        // ZCL 기본 명령의 출발 엔드포인트 지정 (해당 스위치 장치의 엔드포인트)
        cmd_req.zcl_basic_cmd.src_endpoint = HA_ONOFF_SWITCH_ENDPOINT;

        // 주소 모드를 "대상 주소/엔드포인트 없음"으로 설정 (브로드캐스트 또는 바인딩된 장치로 전송됨)
        cmd_req.address_mode = ESP_ZB_APS_ADDR_MODE_DST_ADDR_ENDP_NOT_PRESENT;

        // 실제 전송할 On/Off 명령을 "Toggle"로 지정
        cmd_req.on_off_cmd_id = ESP_ZB_ZCL_CMD_ON_OFF_TOGGLE_ID;

        // Zigbee 스택 리소스를 보호하기 위해 락을 걸고 명령 전송
        esp_zb_lock_acquire(portMAX_DELAY);
        esp_zb_zcl_on_off_cmd_req(&cmd_req);   // On/Off 명령 요청 실행
        esp_zb_lock_release();

        // 빠른 로깅 (부팅 초기 로그에도 찍히도록 EARLY_LOG 사용)
        ESP_EARLY_LOGI(TAG, "Send 'on_off toggle' command");
    }
}

 

 

SDK 함수를 이용하면 편리하긴 하지만 사용하가 원하는 형태로 보내거나 정의되지 않은 장치에 대해서는 불편함이 많은것 같다. STM32WB의 경우 대분의 예제가 Custom 형태로 패킷을 생성하기 때문에 상태 정보나, 문자열등 원하는 정보를 보낼 수 있어서 개발의 편리성이 있었던것 같다.

룰론 표준화된 프로토콜을 사용하면 쉽고 간단히 원하는 장치를 구현할수 있는 장점도 있다.

 

이렇게 해서 실행하면 End Device를 검색하고 바인딩 해서 스위치 누를때 마다 장치의 LED가 On/Off 되는것을 확인 할 수 있다.

ESP32의 장점대로 SDK가 상당히 깔끔하게 잘 구현되어 있어 Zigbee 장치를 빠르고 편리하게 제작할수 있는것 같다.

다만 End Device의 경우 저전력 요구가 많은데 ESP32C6의 저전력 기능은 상용화 장치를 만들기는 부족하점이 많은것 같다.

저전력이 필요 없는 End Device나 Gateway 장치 제작에 있어서는 충분한 장점이 있는것 같다.

반응형