본문 바로가기

ESPRESSIF/ESP32-C6

[ESP32C6 xBee] Zigbee Gateway 제작 - Light ZB End Device 붙이기

ESP32C6는 다른 Zigbee 디바이스들(https://nexp.tistory.com/3861) 과 비교해 저전력 구동에 있어 여러 단점이 있다. 다른 디바이스들은 슬립모드에서 nA 단위로 설명하는데, ESP32C6는 uA단위로 이야기 하고 있다. 3~4년 적어도 5년 이상을 구동해야하는 Zigbee 디바이스 특성상 최대한 소모 전류를 줄여야 하기에 ESP32시리즈로 저전력 구동은 어려운 점이 많다.  일단 동작 전원이 3V 이상이라 더이상 손쓰기가 어려울것 같다.

그래서 ESP32C6로 ZigBee 프로젝트를 진행한다면 전원에 민감하지 않는 Gateway 장치를 만들기에 적합하지 않을까.. WiFi, BLE도 있어 저렴하게 구성하기에 좋을것 같다.

 

Gateway 예제코드를 베이스로 해서 ZigBee Gateway 장치를 만들어 보자



코드 분석부터 해보자

우선 필수 코드만 보면  간단하다. 이전에 작성 했던 Switch Coordinator Device 코드와 비교해서 비교적 비슷하다.

//1) Zigbee 플랫폼 구성 구조체 초기화 메인 함수
void app_main(void)
{
    // 라디오(Radio)와 호스트(Host) 설정을 기본 매크로로 초기화
    esp_zb_platform_config_t config = {
        .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(),  // Zigbee RF 설정 (ESP32C6 내장 Zigbee 라디오)
        .host_config  = ESP_ZB_DEFAULT_HOST_CONFIG(),   // Zigbee 호스트 설정 (기본 호스트 모드)
    };

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

    // NVS(Non-Volatile Storage) 초기화 (플래시 메모리에 파라미터 저장용)
    ESP_ERROR_CHECK(nvs_flash_init());

    // 네트워크 인터페이스 초기화 (Wi-Fi, 이더넷 등 네트워킹 스택 준비)
    ESP_ERROR_CHECK(esp_netif_init());

    // 이벤트 루프 생성 (시스템 이벤트 처리를 위한 기본 루프)
    ESP_ERROR_CHECK(esp_event_loop_create_default());


    // Zigbee 메인 태스크 생성
    // - 태스크 이름: "Zigbee_main"
    // - 스택 크기: 8192 bytes
    // - 우선순위: 5
    // - 핸들: NULL (필요 없음)
    xTaskCreate(esp_zb_task, "Zigbee_main", 8192, NULL, 5, NULL);
}


//2)ESP32-C6에서 Zigbee Coordinator/Gateway 역할을 수행하는 핵심 태스크 생성
static void esp_zb_task(void *pvParameters)
{
//Zigbee Coordinator 역할 초기화 → 클러스터/엔드포인트 등록 → 네트워크 시작 → 메인 루프 실행
}


//3)Zigbee Coordinator(게이트웨이) 동작 시 발생하는 주요 이벤트를 처리
void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct)
{
//네트워크 초기화 → 포메이션 → steering → 신규 장치 announce → permit join
}

//BDB(기본 장치 동작) commissioning을 시작하는 콜백 함수
static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask)
{
ESP_RETURN_ON_FALSE(esp_zb_bdb_start_top_level_commissioning(mode_mask) == ESP_OK, , TAG, "Failed to start Zigbee bdb commissioning");
}

 

 

기존 스위치 코디네이터 장치 코드와  비교하면 뭔가 부족한것 같다.

장치 확인만하고 실제 동작은 하지 않는 코드 같아 보인다. 아무래도 장치를 추가 해가면서 코드를 완성해야 할것 같다.

 

Zigbee 스택 초기화 함수에서 Gateway로 설정하고 EP를 등록하는 등의 절차는 기존 코드와 동일하다.

static void esp_zb_task(void *pvParameters)
{
    /* ----------------- Zigbee 스택 초기화 ----------------- */
    // Coordinator(네트워크 생성자) 역할로 동작하는 Zigbee 네트워크 설정
    esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZC_CONFIG();
    esp_zb_init(&zb_nwk_cfg);


    // 게이트웨이가 운영할 Zigbee 채널 마스크 설정
    esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK);

    /* ----------------- 엔드포인트 / 클러스터 등록 ----------------- */
    // 엔드포인트 리스트 생성 (ZCL Endpoints: Zigbee 장치의 Application Layer 엔드포인트)
    esp_zb_ep_list_t *ep_list = esp_zb_ep_list_create();

    // 클러스터 리스트 생성 (클러스터 = 기능 단위, OnOff, Identify 등)
    esp_zb_cluster_list_t *cluster_list = esp_zb_zcl_cluster_list_create();

    // 게이트웨이 엔드포인트 설정
    esp_zb_endpoint_config_t endpoint_config = {
        .endpoint = ESP_ZB_GATEWAY_ENDPOINT,         // 게이트웨이 전용 Endpoint ID
        .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID,   // Home Automation 프로파일
        .app_device_id = ESP_ZB_HA_REMOTE_CONTROL_DEVICE_ID, // 원격 제어 디바이스 타입
        .app_device_version = 0,
    };

    /* ----------------- Basic 클러스터 (장치 정보) ----------------- */
    // Basic cluster 생성 (Manufacturer, Model 정보 포함)
    esp_zb_attribute_list_t *basic_cluster = esp_zb_basic_cluster_create(NULL);

    // 제조사 이름 속성 추가
    esp_zb_basic_cluster_add_attr(basic_cluster,
                                  ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID,
                                  ESP_MANUFACTURER_NAME);

    // 모델명 속성 추가
    esp_zb_basic_cluster_add_attr(basic_cluster,
                                  ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID,
                                  ESP_MODEL_IDENTIFIER);

    // Basic cluster를 서버(Server role)로 등록
    esp_zb_cluster_list_add_basic_cluster(cluster_list, basic_cluster,
                                          ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);

    // Identify cluster 등록 (네트워크에서 장치 식별할 때 사용)
    esp_zb_cluster_list_add_identify_cluster(cluster_list,
                                             esp_zb_identify_cluster_create(NULL),
                                             ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);

    /* ----------------- 엔드포인트 리스트에 게이트웨이 엔드포인트 추가 ----------------- */
    esp_zb_ep_list_add_gateway_ep(ep_list, cluster_list, endpoint_config);

    // Zigbee 디바이스(ZC, Gateway) 등록 완료
    esp_zb_device_register(ep_list);

    /* ----------------- Zigbee 네트워크 시작 ----------------- */
    ESP_ERROR_CHECK(esp_zb_start(false)); // false → Blocking 모드 아님

    // Zigbee 메인 루프 (네트워크 이벤트/메시지 처리)
    esp_zb_stack_main_loop();

    /* ----------------- 종료 처리 ----------------- */
    esp_rcp_update_deinit();  // RCP 업데이트 모듈 종료
    vTaskDelete(NULL);        // 태스크 삭제
}

 

 

 

esp_zb_app_signal_handler 함수도 동일한데 새로운 장치가 연결을 시도할 때 (ESP_ZB_ZDO_SIGNAL_DEVICE_ANNCE) 처리하는 부분에서 정보 출력만 해주고 있는데… 

여기서 각 End Device에 맞는 바인딩 처리를 해주 면 된다.

/* Zigbee 스택에서 발생하는 다양한 시그널(이벤트)을 처리하는 핸들러 */
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; // 장치 announce 파라미터

    switch (sig_type) {
    /* 초기화 시그널: Zigbee 스택 부팅 후 첫 동작 */
    case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP:
#if CONFIG_EXAMPLE_CONNECT_WIFI
        /* Wi-Fi 연결 시도 (Zigbee Gateway는 보통 MQTT 브로커와 연동) */
        ESP_RETURN_ON_FALSE(example_connect() == ESP_OK, , TAG, "Failed to connect to Wi-Fi");
#if CONFIG_ESP_COEX_SW_COEXIST_ENABLE
        /* Zigbee + Wi-Fi 공존 모드에서 전력 절약 모드 설정 */
        ESP_RETURN_ON_FALSE(esp_wifi_set_ps(WIFI_PS_MIN_MODEM) == ESP_OK, , TAG, "Failed to set Wi-Fi minimum modem power save type");
        esp_coex_wifi_i154_enable();  // Wi-Fi와 802.15.4(Zigbee) 공존 활성화
#else
        ESP_RETURN_ON_FALSE(esp_wifi_set_ps(WIFI_PS_NONE) == ESP_OK, , TAG, "Failed to set Wi-Fi no power save type");
#endif
#endif
        ESP_LOGI(TAG, "Initialize Zigbee stack");
        /* Zigbee BDB 초기화 시작 */
        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, "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 {
                /* 기존 네트워크 유지 → 네트워크 오픈(permit join 180초) */
                esp_zb_bdb_open_network(180);
                ESP_LOGI(TAG, "Device rebooted");
            }
        } else {
            /* 실패 시 로그 및 재시도 (1초 후 초기화 모드로 재시작) */
            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 ieee_address;
            esp_zb_get_long_address(ieee_address); // Coordinator IEEE 주소 획득
            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)",
                     ieee_address[7], ieee_address[6], ieee_address[5], ieee_address[4],
                     ieee_address[3], ieee_address[2], ieee_address[1], ieee_address[0],
                     esp_zb_get_pan_id(), esp_zb_get_current_channel(), esp_zb_get_short_address());
            /* 네트워크 생성 성공 후 steering(장치 참여 허용) 시작 */
            esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING);
        } else {
            /* 실패 → 1초 후 재시도 */
            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;

    /* 네트워크 steering 결과 (장치 참여 허용 절차) */
    case ESP_ZB_BDB_SIGNAL_STEERING:
        if (err_status == ESP_OK) {
            ESP_LOGI(TAG, "Network steering started");
        }
        break;

    /* 새로운 장치 announce 이벤트 (새 장치 네트워크 합류 시) */
    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);
        break;

    /* Permit join 상태 변경 이벤트 (네트워크가 열렸는지 닫혔는지) */
    case ESP_ZB_NWK_SIGNAL_PERMIT_JOIN_STATUS:
        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;

    /* Production config 준비됨 (공장에서 설정된 제조사 정보) */
    case ESP_ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY:
        ESP_LOGI(TAG, "Production configuration is ready");
        if (err_status == ESP_OK) {
            app_production_config_t *prod_cfg = (app_production_config_t *)esp_zb_app_signal_get_params(p_sg_p);
            if (prod_cfg->version == APP_PROD_CFG_CURRENT_VERSION) {
                ESP_LOGI(TAG, "Manufacturer_code: 0x%x, manufacturer_name:%s", 
                         prod_cfg->manuf_code, prod_cfg->manuf_name);
                /* Zigbee node descriptor에 제조사 코드 반영 */
                esp_zb_set_node_descriptor_manufacturer_code(prod_cfg->manuf_code);
            }
        } else {
            ESP_LOGW(TAG, "Production configuration is not present");
        }
        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;
    }
}

 

 

우선 기존에 제작 했던 Light End Device를 Bind해 보자

기존 Switch 코디네이터 코드에서는 단일 장치만 연결하기위한 코드라 직접 Light 장치를 찾아서 연결 했는데…

 Gateway 장치는 다양한 End Device를 연결하기 위해 연결을 요청한 장치에 대해 정보를 esp_zb_zdo_simple_desc_req() 함수로 요청해 바인딩 할 수 있도록 해 주면 좋을것 같다.

    case ESP_ZB_ZDO_SIGNAL_DEVICE_ANNCE: {
        // 새 장치가 네트워크에 조인했을 때
        esp_zb_zdo_signal_device_annce_params_t *annce =
            (esp_zb_zdo_signal_device_annce_params_t *)esp_zb_app_signal_get_params(p_sg_p);
        uint16_t short_addr = annce->device_short_addr;

        ESP_LOGI(TAG, "ZB Device joined: short=0x%04x", short_addr);

        // Simple Descriptor 요청 → endpoint 정보 얻기
        esp_zb_zdo_simple_desc_req_param_t req = {
            .addr_of_interest = short_addr,
            .endpoint = 1,
        };
        esp_zb_zdo_simple_desc_req(&req, NULL);
        break;
    }

 

 

simple_desc_req의 콜백 함수에서 각 요청 장치에 대한 바인딩 처리를 해주면 된다 


void bind_light_device(int16_t short_addr, uint8_t endpoint, void *user_ctx)
{
    ESP_LOGI(TAG, "Found light device (short_addr=0x%04X, endpoint=%d)", short_addr, endpoint);

    esp_zb_ieee_addr_t ieee_addr;
    esp_zb_ieee_address_by_short(short_addr, ieee_addr);

    esp_zb_zdo_bind_req_param_t bind_req = {0};

    // 출발지(Gateway)
    esp_zb_get_long_address(bind_req.src_address);
    bind_req.src_endp   = HA_ONOFF_SWITCH_ENDPOINT;
    bind_req.cluster_id = ESP_ZB_ZCL_CLUSTER_ID_ON_OFF;

    // 목적지(조명)
    bind_req.dst_addr_mode = ESP_ZB_ZDO_BIND_DST_ADDR_MODE_64_BIT_EXTENDED;
    memcpy(bind_req.dst_address_u.addr_long, ieee_addr, sizeof(esp_zb_ieee_addr_t));
    bind_req.dst_endp      = endpoint;

    // 요청 목적지 (나 자신 = 코디네이터)
    bind_req.req_dst_addr = esp_zb_get_short_address();

    ESP_LOGI(TAG, "Sending bind request for On/Off cluster...");
    esp_zb_zdo_device_bind_req(&bind_req, bind_cb, user_ctx);
}

static void simple_desc_cb(esp_zb_zdp_status_t zdo_status, esp_zb_af_simple_desc_1_1_t *simple_desc, void *user_ctx)
{
    esp_zb_zdo_signal_device_annce_params_t *dev_annce_params = user_ctx;

    // 로그 출력: 장치 단축 주소, 장치 ID 확인
    ESP_LOGW(TAG, "type! : %x, %x", dev_annce_params->device_short_addr, simple_desc->app_device_id );

    // Simple Descriptor 응답이 성공했을 경우에만 처리
    if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) {
        ESP_LOGI(TAG, "Simple desc response: status(%d), device_id(%d), app_version(%d), profile_id(0x%x), endpoint_ID(%d)", 
                 zdo_status,
                 simple_desc->app_device_id,      // 장치 ID (어떤 장치인지 구분용)
                 simple_desc->app_device_version, // 앱 버전
                 simple_desc->app_profile_id,     // 프로파일 ID (예: HA 프로파일)
                 simple_desc->endpoint);          // 엔드포인트 번호

        // 수신한 장치 ID에 따라 분기 처리
        switch (simple_desc->app_device_id) 
        {
        // On/Off Light (전등)
        case ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID:
            ESP_LOGI(TAG, "Bind LIGHT_DEVICE");
            bind_light_device(dev_annce_params->device_short_addr, simple_desc->endpoint, user_ctx);
            return; // 바인딩 후 함수 종료

        // Temperature Sensor (온도 센서)
        case ESP_ZB_HA_TEMPERATURE_SENSOR_DEVICE_ID:
            ESP_LOGI(TAG, "Bind TEMPERATURE_DEVICE");
            bind_temperature_device(dev_annce_params->device_short_addr, simple_desc->endpoint, user_ctx);
            break;

        // IAS Zone (보안 센서, 예: 모션, 열림감지 등)
        case ESP_ZB_HA_IAS_ZONE_ID:
            ESP_LOGI(TAG, "Bind IAS ZONE DEVICE");
            bind_ias_device(dev_annce_params->device_short_addr, simple_desc->endpoint, user_ctx);
            return; // 바인딩 후 함수 종료

        // 지원하지 않는 장치
        default:
            ESP_LOGW(TAG, "Unsupported device type! : %x", simple_desc->app_device_id);
            break;
        }
    }
}

 

 

 

이렇게 해서 실행하면 장치는 등록되는것을 확인 할 수 있다

 

I (1026)  ESP_ZB_GATEWAY: Network(0x546a) is open for 180 seconds

I (5206)  ESP_ZB_GATEWAY: ZDO signal: ZDO Device Update (0x30), status: ESP_OK

I (5216)  ESP_ZB_GATEWAY: New device commissioned or rejoined (short: 0x8f29)

I (5236)  ESP_ZB_GATEWAY: User find cb: response_status:0, address:0x8f29, endpoint:10

I (5256)  ESP_ZB_GATEWAY: Simple desc response: status(0), device_id(256), app_version(0), profile_id(0x104), endpoint_ID(10)

I (5256)  ESP_ZB_GATEWAY: ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID

I (5256)  ESP_ZB_GATEWAY: Found light

 

 

Gatway에서 버튼을 누르면 Light End Device로 데이터를 전송하는 코드를 작성하면 스위치를 누를때 마다 Led가 점등 되는것을 확인 할 수 있다.

static void zb_buttons_handler(switch_func_pair_t *button_func_pair)
{
    if (button_func_pair->func == SWITCH_ONOFF_TOGGLE_CONTROL) {
        /* implemented light switch toggle functionality */
        esp_zb_zcl_on_off_cmd_t cmd_req;
        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;
        cmd_req.on_off_cmd_id = ESP_ZB_ZCL_CMD_ON_OFF_TOGGLE_ID;

        esp_zb_lock_acquire(portMAX_DELAY);
        esp_zb_zcl_on_off_cmd_req(&cmd_req);
        esp_zb_lock_release();
        ESP_EARLY_LOGI(TAG, "Send 'on_off toggle' command");
    }
}

 

 

I (11336)  ESP_ZB_GATEWAY: Send 'on_off toggle' command

I (12696)  ESP_ZB_GATEWAY: Send 'on_off toggle' command

 

 

계속해서 다른 장치도 Gateway에 붙혀볼 예정이다.

반응형