YOGYUI

[PROJ] Matter 재실 감지 클러스터 개발 예제 (ESP32) 본문

PROJECT

[PROJ] Matter 재실 감지 클러스터 개발 예제 (ESP32)

요겨 2024. 2. 29. 17:10
반응형

Matter - Occupancy Sensing Cluster Developing Example using ESP32 SoC

지난 글에서 재실 감지 센서(Occupancy Sensing) 관련 클러스터의 Matter 스펙을 알아봤다

Matter Specification - Occupancy Sensing Cluster

 

Matter Specification - Occupancy Sensing Cluster

Matter :: Occupancy Sensing Cluster The server cluster provides an interface to occupancy sensing functionality, including configuration and provision of notifications of occupancy status. 점유 센서 (혹은 재실 감지 센서, 모션 센서) 디바

yogyui.tistory.com

재실 감지 센서(혹은 점유 센서, 모션 센서)는 홈IoT 분야에서 광범위하게 사용되는 센서로, 사람이 감지되면 전등을 켜는 등 다양한 종류의 자동화를 위해 활용된다

 

Matter 생태계에서도 당연히(?) 재실 감지 센서가 준비되어 있으므로 간단하게 Matter 디바이스를 만들어봤다

1. Hardware

1.1. Main Processor

  • ESP32-WROOM-32E-N4

이번 글에서도 역시 EspressIf사의 ESP32 모듈을 사용

1.2. Sensor Module

Digilent 사에서 만든 Pmod PIR 센서 모듈을 사용했다 (링크)

PIR 센서: Passive InfraRed Sensor, 물체에서 방출 혹은 반사되는 적외선을 감지

 

Pmod PIR은 PaPIRs사의 모션 센서 EKMC1601111이 탑재된 모듈로, 단안정 멀티바이브레이터 IC인 74AHC123ABQ가 함께 내장되어 있어 모션 센서 감지 결과를 하나의 Digital 신호선으로 처리할 수 있어 사용하기 굉장히 편하다 (함께 내장된 가변 저항 값을 변경해 모션 감지 유지 시간을 조정할 수 있다)

https://digilent.com/reference/_media/reference/pmod/pmodpir/pmodpir_sch.pdf

 

동작 전압이 1.8~3.3V라 ESP32와 다이렉트로 연결해도 무리없이 사용가능하므로 별도의 외부 회로가 필요없다!

2. Software

2.1. Software Development Kit (SDK)

2.2. 소스코드 커밋

https://github.com/YOGYUI/matter-esp32-ekmc16

 

GitHub - YOGYUI/matter-esp32-ekmc16: Matter occupancy sensor (ESP32 + EKMC16xx) example

Matter occupancy sensor (ESP32 + EKMC16xx) example - YOGYUI/matter-esp32-ekmc16

github.com

2.3. 센서 I/O 핸들링 클래스 구현

PmodPIR의 GPIO Input을 핸들링하기 위해 CPmodPIRCtrl 클래스를 만들었으며, 아래와 같이 GPIO 설정 및 인터럽트 서비스 루틴 (ISR)을 만들어 Motion Detection 신호의 rising edge, falling edge를 둘 모두를 감지했을 때 콜백 함수를 호출할 수 있게 구현했다

※ TIP: GPIO의 ISR을 제대로 처리하려면 인터럽트 발생 시 진입하는 함수를 IRAM_ATTR 플래그로 함수를 Internal RAM에 상주시켜야 하며, 이 때의 동작을 메인 코드랑 연동하기 위해서는 FreeRTOS의 RTOS Task 및 Queue를 사용해줘야 한다

#include "pmodpirctrl.h"
#include "driver/gpio.h"
#include "definition.h"

CPmodPIRCtrl* CPmodPIRCtrl::_instance = nullptr;

static void IRAM_ATTR gpio_isr_handler(void* arg)
{
    CPmodPIRCtrl *obj = reinterpret_cast<CPmodPIRCtrl*>(arg);
    QueueHandle_t queue = obj->get_queue_isr();
    uint8_t temp = 0;
    xQueueSendFromISR(queue, &temp, nullptr);
}

CPmodPIRCtrl::CPmodPIRCtrl()
{
    m_gpio_num = 0;
    m_callback_ptr = nullptr;
    m_queue_isr = xQueueCreate(10, sizeof(uint8_t));
    m_keepalive = true;
    xTaskCreate(task_gpio_isr_function, "PMOD_PIR_TASK", TASK_STACK_DEPTH, this, 10, &m_task_handle_isr);
}

CPmodPIRCtrl::~CPmodPIRCtrl()
{
    m_keepalive = false;
    gpio_isr_handler_remove((gpio_num_t)m_gpio_num);
    m_callback_ptr = nullptr;
}

bool CPmodPIRCtrl::initialize(uint8_t gpio_num)
{
    esp_err_t ret;
    m_gpio_num = gpio_num;

    gpio_config_t cfg = {};
    cfg.pin_bit_mask = 1ULL << m_gpio_num;
    cfg.mode = GPIO_MODE_INPUT;
    cfg.pull_up_en = GPIO_PULLUP_ENABLE;
    cfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
    cfg.intr_type = GPIO_INTR_ANYEDGE;  // detect both occupancy and no occupancy
    gpio_config(&cfg);
    gpio_install_isr_service(0);
    gpio_isr_handler_add((gpio_num_t)m_gpio_num, gpio_isr_handler, (void*)this);
    return true;
}

void CPmodPIRCtrl::register_callback_gpio_change(fn_gpio_change_callback callback)
{
    m_callback_ptr = std::move(callback);
}

QueueHandle_t CPmodPIRCtrl::get_queue_isr()
{
    return m_queue_isr;
}

void CPmodPIRCtrl::set_callback(uint8_t level)
{
    if (m_callback_ptr) {
        m_callback_ptr(level);
    }
}

void CPmodPIRCtrl::task_gpio_isr_function(void *param)
{
    CPmodPIRCtrl *obj = reinterpret_cast<CPmodPIRCtrl*>(param);
    uint8_t temp = 0;

    while (obj->m_keepalive) {
        if (xQueueReceive(obj->m_queue_isr, (void *)&temp, 1) == pdTRUE) {
            uint8_t level = (uint8_t)gpio_get_level((gpio_num_t)obj->m_gpio_num);
            GetLogger(eLogType::Info)->Log("GPIO Level changed to %u", level);
            obj->set_callback(level);
        }
    }
    vTaskDelete(nullptr);
}

2.4. 재실 감지 센서 엔드포인트 생성

#include "occupancysensor.h"
#include "system.h"

COccupancySensor::COccupancySensor()
{
}

bool COccupancySensor::matter_init_endpoint()
{
    esp_matter::node_t *root = GetSystem()->get_root_node();
    esp_matter::endpoint::occupancy_sensor::config_t config_endpoint;
    config_endpoint.occupancy_sensing.occupancy = 0;
    config_endpoint.occupancy_sensing.occupancy_sensor_type = 0;  // 0 = PIR, 1 = Ultrasonic, 2 = PIRAndUltrasonic, 3 = PhysicalContact
    config_endpoint.occupancy_sensing.occupancy_sensor_type_bitmap = 0x01; // Bit 0 = PIR, Bit 1 = Ultrasonic, Bit 2 = PhysicalContact
    uint8_t flags = esp_matter::ENDPOINT_FLAG_DESTROYABLE;
    m_endpoint = esp_matter::endpoint::occupancy_sensor::create(root, &config_endpoint, flags, nullptr);
    if (!m_endpoint) {
        GetLogger(eLogType::Error)->Log("Failed to create endpoint");
        return false;
    }
    return CDevice::matter_init_endpoint();
}

 

재실 감지 센서 엔드포인트는 esp-matter SDK의 occupancy_sensor::create 함수를 사용하면 간단하게 추가할 수 있다

이 때 사전 설정으로 SensorType 및 SensorTypeBitmap은 PIR 센서에 맞게 값을 넣어줬다 (아래 Matter Spec 참고)

 

void COccupancySensor::update_occupancy(uint8_t value)
{
    m_occupancy = value;
    if (m_occupancy != m_occupancy_prev) {
        GetLogger(eLogType::Info)->Log("Update occupancy as %u", value);
        matter_update_clus_occupancy_attr_occupancy();
    }
    m_occupancy_prev = m_occupancy;
}

void COccupancySensor::matter_update_clus_occupancy_attr_occupancy(bool force_update/*=false*/)
{
    esp_matter_attr_val_t target_value = esp_matter_bitmap8(m_occupancy);
    matter_update_cluster_attribute_common(
        m_endpoint_id,
        chip::app::Clusters::OccupancySensing::Id,
        chip::app::Clusters::OccupancySensing::Attributes::Occupancy::Id,
        target_value,
        &m_matter_update_by_client_clus_occupancy_attr_occupancy,
        force_update
    );
}

 

PIR 센서로부터 모션 감지 여부에 따라 Occupancy 어트리뷰트의 값을 0 혹은 1로 변경해주면 된다

 

메인 코드 (CSystem 클래스)에서 GPIO ISR 인터럽트 결과 콜백과 Matter 어트리뷰터 값 갱신 함수를 연결해주면 된다

bool CSystem::initialize()
{
    /* 생략 */
    GetPmodPIRCtrl()->initialize(GPIO_PIN_SENSOR_SIGNAL);
    GetPmodPIRCtrl()->register_callback_gpio_change(
        std::bind(&CSystem::on_pmod_pir_level_changed, this, std::placeholders::_1));
        
    /* 생략 */
    COccupancySensor *sensor = new COccupancySensor();
    if (sensor && sensor->matter_init_endpoint()) {
        m_device_list.push_back(sensor);
    } else {
        return false;
    }
    
    /* 생략 */
}

void CSystem::on_pmod_pir_level_changed(uint8_t level)
{
    CDevice *dev = find_device_by_endpoint_id(1);
    if (dev) {
        dev->update_occupancy(level);
    }
}

 

3. DEMO

3.1. Google Home 액세서리 추가

Matter 지원 기기 선택 - QR Code 촬영
Matter device commissioning (BLE-WiFi)
디바이스 설정

 

 

디바이스를 추가하면 아래와 같이 간단하게 '감지되지 않음' 라벨이 기재된 액세서리가 디스플레이된다

 

모션이 감지되면 아래와 같이 '감지됨' 라벨이 기재된다

 

3.2. 움직임 감지 예시 (동영상)

갤럭시탭에 설치된 Google Home으로 모션 감지에 대한 기능을 간단하게 테스트해봤다

 

3.3. ESP32 로그

어트리뷰트 값을 간단하게 1 혹은 0으로만 바꿔주면 되기 때문에 별도로 어렵게 해석해야할 로그는 없다~

(Occupancy Sensing 클러스터의 Occupancy 어트리뷰트)


I (181983) logger: [CPmodPIRCtrl::task_gpio_isr_function] GPIO Level changed to 1 [pmodpirctrl.cpp:98]
I (181983) logger: [COccupancySensor::update_occupancy] Update occupancy as 1 [occupancysensor.cpp:51]
I (181993) esp_matter_attribute: ********** W : Endpoint 0x0001's Cluster 0x00000406's Attribute 0x00000000 is 1 **********
I (182003) esp_matter_attribute: ********** R : Endpoint 0x0001's Cluster 0x00000406's Attribute 0x00000000 is 1 **********
I (182013) chip[EM]: <<< [E:41138i S:6039 M:45299765] (S) Msg TX to 1:00000000CB36C304 [C4AA] [UDP:[FE80::EEF8:13B8:2EDB:E3E2%st1]:5540] --- Type 0001:05 (IM:ReportData)
I (182043) chip[EM]: >>> [E:41138i S:6039 M:20733308 (Ack:45299765)] (S) Msg RX from 1:00000000CB36C304 [C4AA] --- Type 0001:01 (IM:StatusResponse)
I (182053) chip[IM]: Received status response, status is 0x00
I (182053) chip[EM]: <<< [E:41138i S:6039 M:45299766 (Ack:20733308)] (S) Msg TX to 1:00000000CB36C304 [C4AA] [UDP:[FE80::EEF8:13B8:2EDB:E3E2%st1]:5540] --- Type 0000:10 (SecureChannel:StandaloneAck)
I (210663) logger: [CPmodPIRCtrl::task_gpio_isr_function] GPIO Level changed to 0 [pmodpirctrl.cpp:98]
I (210663) logger: [COccupancySensor::update_occupancy] Update occupancy as 0 [occupancysensor.cpp:51]
I (210673) esp_matter_attribute: ********** W : Endpoint 0x0001's Cluster 0x00000406's Attribute 0x00000000 is 0 **********
I (210683) esp_matter_attribute: ********** R : Endpoint 0x0001's Cluster 0x00000406's Attribute 0x00000000 is 0 **********
I (210693) chip[EM]: <<< [E:41139i S:6039 M:45299767] (S) Msg TX to 1:00000000CB36C304 [C4AA] [UDP:[FE80::EEF8:13B8:2EDB:E3E2%st1]:5540] --- Type 0001:05 (IM:ReportData)
I (210723) chip[EM]: >>> [E:41139i S:6039 M:20733309 (Ack:45299767)] (S) Msg RX from 1:00000000CB36C304 [C4AA] --- Type 0001:01 (IM:StatusResponse)
I (210723) chip[IM]: Received status response, status is 0x00
I (210733) chip[EM]: <<< [E:41139i S:6039 M:45299768 (Ack:20733309)] (S) Msg TX to 1:00000000CB36C304 [C4AA] [UDP:[FE80::EEF8:13B8:2EDB:E3E2%st1]:5540] --- Type 0000:10 (SecureChannel:StandaloneAck)



다음 예제로는 조도 센서(Illumination sensor)를 한 번 만들어보자~

반응형
Comments