YOGYUI

[PROJ] Matter 온도/상대습도 측정 클러스터 개발 예제 (ESP32) 본문

PROJECT

[PROJ] Matter 온도/상대습도 측정 클러스터 개발 예제 (ESP32)

요겨 2024. 2. 15. 21:26
반응형

 

Matter - Temperature, Relative Humidity Measurement Cluster Developing Example using ESP32 SoC

지난번 글에서Temperature Measurement(온도 측정)와 Relative Humidity Measurement(상대 습도 측정)두 Matter Cluster에 대한 Specification을 알아봤다

Matter Specification - Temperature Measurement Cluster

 

Matter Specification - Temperature Measurement Cluster

Matter :: Temperature Measurement Cluster This cluster provides an interface to temperature measurement functionality, including configuration and provision of notifications of temperature measurements. 온도 측정 센서를 위한 클러스터 1. Classi

yogyui.tistory.com

Matter Specification - Water Content Measurement Clusters

 

Matter Specification - Water Content Measurement Clusters

Matter :: Water Content Measurement Clusters This is a base cluster. The server cluster provides an interface to water content measurement functionality. The measurement is reportable and may be configured for reporting. Water content measurements include,

yogyui.tistory.com

원래는 Matter Cluster(클러스터) 하나당 예제 프로젝트 하나씩 진행할 계획이었는데, 내가 가지고 있는 센서 모듈들이 모두 온도와 상대습도를 함께 측정할 수 있는 모듈들이라 그냥 Endpoint(엔드포인트) 두 개를 추가하는 예제를 만들어봤다

1. Hardware

1.1. Main Processor

  • ESP32-WROOM-32E-N4

현재 내가 Matter 개발 관련해서 가장 많이 다루고 있는 SoC는 EspressIf사의 ESP32 모듈이라 이번 예제에서도 빠른 코드 개발을 위해 ESP32 모듈을 채택! (Thread 관련 프로젝트에서는 Nordic사의 nRF 모듈을 주로 사용하는데, 개발 편의성은 둘 다 매우 좋다고 생각한다)

1.2. Sensor Module

센서는 Silicon Labs사의 Si7021을 사용했으며, 모듈은 DFRobot의 제품을 사용했다

https://wiki.dfrobot.com/SI7021_Temperature_and_humidity_sensor_SKU_TOY0054

 

Si7021 데이터시트: https://www.silabs.com/documents/public/data-sheets/Si7021-A20.pdf

 

I2C 인터페이스로 MCU와 데이터를 주고 받는데, Hold/No Hold Measurement 기능의 차이만 있을 뿐 온도와 상대습도 측정의 기본 기능에 충실하기 때문에 초심자가 온습도 센서 프로젝트를 시작하기에 적합한 IC이다

(I2C 인터페이스를 학습할 때 사용하기에도 적합)

2. Software

2.1. Software Development Kit (SDK)

ESP32로 Matter 관련 개발할 때는 위 3개 SDK를 사용하면 된다 (2024년 2월 9일 기준 최신 커밋)

2.2. 소스코드 커밋

언제나처럼(?) 예제 개발 코드는 GitHub(깃허브)에 커밋 완료

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

 

GitHub - YOGYUI/matter-esp32-si7021

Contribute to YOGYUI/matter-esp32-si7021 development by creating an account on GitHub.

github.com

2.3. I2C 통신 클래스 구현

Inter-Integrated Circuit(I2C) 통신 인터페이스는 ESP-IDF SDK의 문서를 읽으면 누구나 쉽게 구현할 수 있다

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2c.html

 

Inter-Integrated Circuit (I2C) - ESP32 - — ESP-IDF Programming Guide latest documentation

Perform a write transaction on the I2C bus. The transaction will be undergoing until it finishes or it reaches the timeout provided. Note If a callback was registered with i2c_master_register_event_callbacks, the transaction will be asynchronous, and thus,

docs.espressif.com

기본 기능(Read/Write)에 충실한 CI2CMaster 클래스를 아래와 같이 구현했다 (여러 포트의 I2C를 사용할 수 있다는 가정 하에 마스터 포트에 대한 객체 지향 프로그래밍)

#include "I2CMaster.h"
#include "driver/i2c.h"

CI2CMaster::CI2CMaster()
{
    m_port = 0;
}

CI2CMaster::~CI2CMaster()
{
}

bool CI2CMaster::initialize(int port, int gpio_scl, int gpio_sda, uint32_t clk_speed)
{
    m_port = port;
    i2c_config_t i2c_conf;
    i2c_conf.mode = I2C_MODE_MASTER;
    i2c_conf.sda_io_num = gpio_sda;
    i2c_conf.scl_io_num = gpio_scl;
    i2c_conf.sda_pullup_en = GPIO_PULLUP_ENABLE,
    i2c_conf.scl_pullup_en = GPIO_PULLUP_ENABLE,
    i2c_conf.master.clk_speed = clk_speed,
    i2c_conf.clk_flags = 0;

    i2c_param_config(m_port, &i2c_conf);
    i2c_driver_install(m_port, I2C_MODE_MASTER, 0, 0, 0);
    return true;
}

bool CI2CMaster::release()
{
    i2c_port_t port = (i2c_port_t)m_port;
    return true;
}

bool CI2CMaster::write_bytes(uint8_t dev_addr, uint8_t *data, size_t data_len, uint32_t timeout_ms/*=1000*/)
{
    i2c_master_write_to_device(m_port, dev_addr, data, data_len, timeout_ms / portTICK_PERIOD_MS);
    return true;
}

bool CI2CMaster::read_bytes(uint8_t dev_addr, uint8_t *data, size_t data_len, uint32_t timeout_ms/*=1000*/)
{
    i2c_master_read_from_device(m_port, dev_addr, data, data_len, timeout_ms / portTICK_PERIOD_MS);
    return true;
}

bool CI2CMaster::write_and_read_bytes(uint8_t dev_addr, uint8_t *data_write, size_t data_write_len, uint8_t *data_read, size_t data_read_len, uint32_t timeout_ms/*=1000*/)
{
    i2c_master_write_read_device(m_port, dev_addr, data_write, data_write_len, data_read, data_read_len, timeout_ms / portTICK_PERIOD_MS);
    return true;
}

2.4. 온습도 센서 클래스 구현

Si7021 센서를 가리키는 클래스 CSi7021Ctrl 클래스는 아래와 같이 CI2CMaster 객체를 멤버변수로 갖게 만들어 I2C 통신 인터페이스는 모두 자체적으로 처리하도록 구현했다

#include "si7021.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define SI7021_DEFAULT_ADDRESS      0x40
#define SI7021_MEASRH_NOHOLD_CMD    0xF5    /**< Measure Relative Humidity, No Hold Master Mode */
#define SI7021_MEASTEMP_NOHOLD_CMD  0xF3    /**< Measure Temperature, No Hold Master Mode */
#define SI7021_RESET_CMD            0xFE    /**< Reset Command */

bool CSi7021Ctrl::initialize(CI2CMaster *i2c_master)
{
    m_i2c_master = i2c_master;
    return true;
}

bool CSi7021Ctrl::release()
{
    return true;
}

bool CSi7021Ctrl::reset()
{
    uint8_t data[1] = {SI7021_RESET_CMD};
    return m_i2c_master->write_bytes(SI7021_DEFAULT_ADDRESS, data, 1);
}

bool CSi7021Ctrl::read_temperature(float *temperature)
{
    uint8_t data_write[1] = {SI7021_MEASTEMP_NOHOLD_CMD};
    m_i2c_master->write_bytes(SI7021_DEFAULT_ADDRESS, data_write, 1);
    vTaskDelay(20 / portTICK_PERIOD_MS);

    uint8_t data_read[3] = {0,};
    if (!m_i2c_master->read_bytes(SI7021_DEFAULT_ADDRESS, data_read, 3)) {
        return false;
    }
    uint16_t temp = data_read[0] << 8 | data_read[1];
    float conv = (float)temp * 175.72 / 65536. - 46.85;
    *temperature = conv;

    return true;
}

bool CSi7021Ctrl::read_humidity(float *humidity)
{
    uint8_t data_write[1] = {SI7021_MEASRH_NOHOLD_CMD};
    m_i2c_master->write_bytes(SI7021_DEFAULT_ADDRESS, data_write, 1);
    vTaskDelay(20 / portTICK_PERIOD_MS);

    uint8_t data_read[3] = {0,};
    if (!m_i2c_master->read_bytes(SI7021_DEFAULT_ADDRESS, data_read, 3)) {
        return false;
    }
    uint16_t temp = data_read[0] << 8 | data_read[1];
    float conv = (float)temp * 125. / 65536. - 6.;
    *humidity = conv > 100.f? 100.f : conv;

    return true;
}

2.5. 온도 센서 Endpoint 생성

온도 측정 센서에 대한 Matter 스펙 중 일부는 아래와 같다

  • Device Type ID: 0x0302
  • 필수 구현 클러스터
    • Temperature Measurement (ID: 0x0402)
    • Identify (ID: 0x0003)

esp-matter SDK에서 temperature_sensor 네임스페이스로 엔드포인트 추가 시 위 클러스터를 함께 추가해주기 때문에 아래와 같이 손쉽게 온도 센서 엔드포인트를 추가할 수 있다 (Endpoint ID 0인 Root Node의 추가에 대해서는 system.cpp 코드 참고)

#include <esp_matter.h>
#include <esp_matter_core.h>

bool CTemperatureSensor::matter_init_endpoint()
{
    esp_matter::node_t *root = GetSystem()->get_root_node();
    esp_matter::endpoint::temperature_sensor::config_t config_endpoint;
    config_endpoint.temperature_measurement.min_measured_value = -1000; // -10 * 100
    config_endpoint.temperature_measurement.max_measured_value = 8500;  // +85 * 100
    uint8_t flags = esp_matter::ENDPOINT_FLAG_DESTROYABLE;
    m_endpoint = esp_matter::endpoint::temperature_sensor::create(root, &config_endpoint, flags, nullptr);

    return CDevice::matter_init_endpoint();
}

 

Si7021의 온도 측정 범위는 -10℃ ~ 85℃ 이기 때문에 Min/Max Measure Value 클러스터의 값도 엔드포인트 및 클러스터 초기화때 지정해줬다

[중요]

Matter의 Temperature 데이터는 아래와 같이 정의된다

측정된 섭씨 온도에 100을 곱하여 소수점 아래는 자른 2바이트 부호있는 정수 자료형(int16_t)으로 취급해야 한다 (온도 측정 resolution은 0.01℃)

 

따라서 어트리뷰트(Attribute) 갱신(update) 구문은 아래와 같이 간단하게 현재 측정된 온도에 100을 곱한 뒤 정수형으로 바꿔 업데이트하면 된다

void CTemperatureSensor::update_measured_value(float value)
{
    m_measured_value = value;
    matter_update_clus_tempmeasure_attr_measureval();
}

void CTemperatureSensor::matter_update_clus_tempmeasure_attr_measureval(bool force_update/*=false*/)
{
    esp_matter_attr_val_t target_value = esp_matter_nullable_int16(
        int16_t(m_measured_value * 100.f));
    matter_update_cluster_attribute_common(
        m_endpoint_id,
        chip::app::Clusters::TemperatureMeasurement::Id,
        chip::app::Clusters::TemperatureMeasurement::Attributes::MeasuredValue::Id,
        target_value,
        &m_matter_update_by_client_clus_tempmeasure_attr_measureval,
        force_update
    );
}

2.6. 습도 센서 Endpoint 생성

상대 습도 측정 센서에 대한 Matter 스펙 중 일부는 아래와 같다

  • Device Type ID: 0x0307
  • 필수 구현 클러스터
    • Relative Humidity Measurement (ID: 0x0402)
    • Identify (ID: 0x0003)

온도 센서 엔드포인트와 유사하게 esp-matter의 humidity_sensor 네임스페이스로 엔드포인트를 손쉽게 추가할 수 있다

bool CHumiditySensor::matter_init_endpoint()
{
    esp_matter::node_t *root = GetSystem()->get_root_node();
    esp_matter::endpoint::humidity_sensor::config_t config_endpoint;
    uint8_t flags = esp_matter::ENDPOINT_FLAG_DESTROYABLE;
    esp_matter::endpoint::humidity_sensor::create(root, &config_endpoint, flags, nullptr);
    return CDevice::matter_init_endpoint();
}

 

[중요] 상대 습도 측정값은 온도와는 다르게 부호없는 2바이트 정수형(uint16_t)로 취급해야 한다 (당연한 말이지만, 상대 습도는 0% ~ 100% 범위의 값만 가질 수 있다)

상대 습도값 또한 온도와 똑같이 100을 곱하여 해상도(resolution)가 0.01%이 된다

온도 센서와 유사하게 측정값 어트리뷰트 갱신 메서드를 아래와 같이 구현해줬다

void CHumiditySensor::update_measured_value(float value)
{
    m_measured_value = value;
    matter_update_clus_relhummeasure_attr_measureval();
}

void CHumiditySensor::matter_update_clus_relhummeasure_attr_measureval(bool force_update/*=false*/)
{
    esp_matter_attr_val_t target_value = esp_matter_nullable_uint16(
        uint16_t(m_measured_value * 100.f));
    matter_update_cluster_attribute_common(
        m_endpoint_id,
        chip::app::Clusters::RelativeHumidityMeasurement::Id,
        chip::app::Clusters::RelativeHumidityMeasurement::Attributes::MeasuredValue::Id,
        target_value,
        &m_matter_update_by_client_clus_relhummeasure_attr_measureval,
        force_update
    );
}

2.7. Measurement Task 생성

주기적으로 센서에서 값을 측정한 뒤 어트리뷰트 값 갱신을 위해 FreeRTOS 태스크(Task)를 아래와 같이 구현했다

#include "system.h"
#include "si7021.h"
#include "temperaturesensor.h"
#include "humiditysensor.h"

#define TASK_TIMER_STACK_DEPTH  3072
#define TASK_TIMER_PRIORITY     5
#define MEASURE_PERIOD_US       10000000

CSystem::CSystem() 
{
    xTaskCreate(task_timer_function, "TASK_TIMER", TASK_TIMER_STACK_DEPTH, this, TASK_TIMER_PRIORITY, &m_task_timer_handle);
}

void CSystem::task_timer_function(void *param)
{
    CSystem *obj = static_cast<CSystem *>(param);
    int64_t current_tick_us;
    int64_t last_tick_us = 0;
    float measure_temperature, measure_humidity;
    CDevice *dev;

    while (1) {
        current_tick_us = esp_timer_get_time();
        if (current_tick_us - last_tick_us >= MEASURE_PERIOD_US) {
            if (GetSi7021Ctrl()->read_temperature(&measure_temperature)) {
                dev = obj->find_device_by_endpoint_id(1);
                if (dev) {
                    dev->update_measured_value(measure_temperature);
                }
            }
            vTaskDelay(pdMS_TO_TICKS(10));
            if (GetSi7021Ctrl()->read_humidity(&measure_humidity)) {
                dev = obj->find_device_by_endpoint_id(2);
                if (dev) {
                    dev->update_measured_value(measure_humidity);
                }
            }
            last_tick_us = current_tick_us;
        }
        vTaskDelay(pdMS_TO_TICKS(50));
    }
    vTaskDelete(nullptr);
}

10초에 한번씩 값을 읽은 뒤 온도/습도 각각 어트리뷰트 갱신 메서드를 호출하도록 구현

(측정 주기를 바꾸려면 MEASURE_PERIOD_US 정의를 바꾸면 된다)

※ 예제에서는 측정값을 읽자마자 어트리뷰트 값을 갱신하게 구현했는데, 보통은 5~10번정도 읽은 뒤 평균값을 갱신하는 방법을 주로 사용하니 다른 예제 코드도 많이 참고하도록 한다

3. DEMO

3.1. Apple Home 액세서리 추가

Apple Home에 매터 액세서리를 추가해준다 (Apple Home 허브는 HomePod을 사용했으며, Matter 개발자용 프로파일을 심어둔 상태)

Matter 스펙은 온도는 0.01℃, 상대 습도는 0.01%의 해상도인데, Apple Home에서는 온도는 0.5℃, 상대습도는 1%의 해상도라 Apple Home만을 위한 제품을 생각한다면 측정값을 자주 갱신하지 않아도 되게 구현할 수 있다

(Google Home도 해상도가 그렇게 높지 않았던 걸로 기억한다)

3.2. ESP32 로그 

주기적으로 값이 갱신될 때 ESP32 내부 로그는 다음과 같이 기록된다


I (91068) logger: [CTemperatureSensor::update_measured_value] Update measured temperature value as 23.1849 [temperaturesensor.cpp:52]
I (91068) esp_matter_attribute: ********** W : Endpoint 0x0001's Cluster 0x00000402's Attribute 0x00000000 is 2318 **********
I (91078) esp_matter_attribute: ********** R : Endpoint 0x0001's Cluster 0x00000402's Attribute 0x00000000 is 2318 **********
I (91098) chip[EM]: <<< [E:25541i S:45367 M:257981178] (S) Msg TX to 1:73D697EBCDD128E2 [1927] [UDP:[FE80::852:8990:CD91:F23B%st1]:54967] --- Type 0001:05 (IM:ReportData)
I (91108) logger: [CHumiditySensor::update_measured_value] Update measured relative humidity value as 60.2956 [humiditysensor.cpp:50]
I (91118) esp_matter_attribute: ********** W : Endpoint 0x0002's Cluster 0x00000405's Attribute 0x00000000 is 6029 **********
I (91148) chip[EM]: >>> [E:25541i S:45367 M:250464984 (Ack:257981178)] (S) Msg RX from 1:73D697EBCDD128E2 [1927] --- Type 0001:01 (IM:StatusResponse)
I (91158) chip[IM]: Received status response, status is 0x00
I (91168) chip[EM]: <<< [E:25541i S:45367 M:257981179 (Ack:250464984)] (S) Msg TX to 1:73D697EBCDD128E2 [1927] [UDP:[FE80::852:8990:CD91:F23B%st1]:54967] --- Type 0000:10 (SecureChannel:StandaloneAck)
I (91188) esp_matter_attribute: ********** R : Endpoint 0x0002's Cluster 0x00000405's Attribute 0x00000000 is 6029 **********
I (91198) chip[EM]: <<< [E:25542i S:45367 M:257981180] (S) Msg TX to 1:73D697EBCDD128E2 [1927] [UDP:[FE80::852:8990:CD91:F23B%st1]:54967] --- Type 0001:05 (IM:ReportData)
I (91228) chip[EM]: >>> [E:25542i S:45367 M:250464985 (Ack:257981180)] (S) Msg RX from 1:73D697EBCDD128E2 [1927] --- Type 0001:01 (IM:StatusResponse)
I (91238) chip[IM]: Received status response, status is 0x00
I (91248) chip[EM]: <<< [E:25542i S:45367 M:257981181 (Ack:250464985)] (S) Msg TX to 1:73D697EBCDD128E2 [1927] [UDP:[FE80::852:8990:CD91:F23B%st1]:54967] --- Type 0000:10 (SecureChannel:StandaloneAck)
I (101098) logger: [CTemperatureSensor::update_measured_value] Update measured temperature value as 23.1742 [temperaturesensor.cpp:52]
I (101098) esp_matter_attribute: ********** W : Endpoint 0x0001's Cluster 0x00000402's Attribute 0x00000000 is 2317 **********
I (101108) esp_matter_attribute: ********** R : Endpoint 0x0001's Cluster 0x00000402's Attribute 0x00000000 is 2317 **********
I (101128) chip[EM]: <<< [E:25543i S:45367 M:257981182] (S) Msg TX to 1:73D697EBCDD128E2 [1927] [UDP:[FE80::852:8990:CD91:F23B%st1]:54967] --- Type 0001:05 (IM:ReportData)
I (101138) logger: [CHumiditySensor::update_measured_value] Update measured relative humidity value as 60.494 [humiditysensor.cpp:50]
I (101148) esp_matter_attribute: ********** W : Endpoint 0x0002's Cluster 0x00000405's Attribute 0x00000000 is 6049 **********
I (101188) chip[EM]: >>> [E:25543i S:45367 M:250464986 (Ack:257981182)] (S) Msg RX from 1:73D697EBCDD128E2 [1927] --- Type 0001:01 (IM:StatusResponse)
I (101188) chip[IM]: Received status response, status is 0x00
I (101198) chip[EM]: <<< [E:25543i S:45367 M:257981183 (Ack:250464986)] (S) Msg TX to 1:73D697EBCDD128E2 [1927] [UDP:[FE80::852:8990:CD91:F23B%st1]:54967] --- Type 0000:10 (SecureChannel:StandaloneAck)
I (101218) esp_matter_attribute: ********** R : Endpoint 0x0002's Cluster 0x00000405's Attribute 0x00000000 is 6049 **********
I (101238) chip[EM]: <<< [E:25544i S:45367 M:257981184] (S) Msg TX to 1:73D697EBCDD128E2 [1927] [UDP:[FE80::852:8990:CD91:F23B%st1]:54967] --- Type 0001:05 (IM:ReportData)
I (101268) chip[EM]: >>> [E:25544i S:45367 M:250464987 (Ack:257981184)] (S) Msg RX from 1:73D697EBCDD128E2 [1927] --- Type 0001:01 (IM:StatusResponse)
I (101278) chip[IM]: Received status response, status is 0x00
I (101278) chip[EM]: <<< [E:25544i S:45367 M:257981185 (Ack:250464987)] (S) Msg TX to 1:73D697EBCDD128E2 [1927] [UDP:[FE80::852:8990:CD91:F23B%st1]:54967] --- Type 0000:10 (SecureChannel:StandaloneAck)



Matter 클러스터들 중 센서 종류는 큰 어려움없이 구현할 수 있는 것들이 대부분이라, 앞으로 당분간 센서 위주로 매터 예제 프로젝트를 만들어볼까 한다 ㅎㅎ

끝~!

반응형
Comments