YOGYUI

[PROJ] Matter 압력(대기압) 측정 클러스터 개발 예제 (ESP32) 본문

PROJECT

[PROJ] Matter 압력(대기압) 측정 클러스터 개발 예제 (ESP32)

요겨 2024. 2. 23. 08:32
반응형

Matter - Pressure Measurement Cluster Developing Example using ESP32 SoC

지난 글에서 압력 측정(Pressure Measurement) 센서 관련 클러스터의 Matter 스펙에 대해 알아봤다

Matter Specification - Pressure Measurement Cluster

 

Matter Specification - Pressure Measurement Cluster

Matter :: Pressure Measurement Cluster This cluster provides an interface to pressure measurement functionality, including configuration and provision of notifications of pressure measurements. 압력 측정 센서 (ex: 기압계) 디바이스를 위한

yogyui.tistory.com

우리가 일상 생활에서 흔히 접할 수 있는 압력 센서는 바로미터(barometer)라고도 불리는 기압계로, Matter도 홈 IoT 디바이스가 타겟인만큼 기압계를 타겟으로 압력 센서가 설계된 것을 알 수 있다 

 

기압계 센서 모듈을 사용해서 Matter 디바이스를 만들어보자

1. Hardware

1.1. Main Processor

  • ESP32-WROOM-32E-N4

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

1.2. Sensor Module

3년전 블로그 초창기에 Bosch Sensortec사의 제품인 BMP180 센서 모듈을 소개한 적이 있다 

DFRobot BMP180 Barometer Sensor

 

DFRobot BMP180 Barometer Sensor

1. Hardware 독일 Bosch사(오...대기업...)에서 제작한 BMP180 기압계(barometer)가 장착된 모듈 DFRobot 공식 소개 페이지: SKU:TOY0058(단종되었다...) I2C 시리얼 인터페이스로 통신하며, 0.12hPa/m 고정밀 기압 측

yogyui.tistory.com

 

다른 센서 모듈에 비해 사용법이 조금 더 어려우나, (calibration 데이터를 읽고, 몇단계의 수식을 거쳐야 압력을 계산할 수 있다) 아두이노 라이브러리가 잘 구현되어 있는 것들이 많으니 참고할 수 있다

압력 측정 범위는 300 ~ 1100hPa 

※ DFRobot의 모듈은 단종되었다고 한다 ㅎㅎ... Adafruit의 제품 등으로 대체할 수 있다

2. Software

2.1. Software Development Kit (SDK)

2.2. 소스코드 커밋

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

 

GitHub - YOGYUI/matter-esp32-bmp180: Matter pressure measurement sensor (ESP32 + BMP180) example

Matter pressure measurement sensor (ESP32 + BMP180) example - YOGYUI/matter-esp32-bmp180

github.com

2.3. I2C 통신 클래스 구현

지난 번 온습도계 센서(링크) 및 이산화탄소 농도 측정 센서(링크)에서 사용한 I2CMaster 클래스를 이번에도 그대로 사용했다

2.4. 기압계 (BMP180) 클래스 구현

BMP180은 I2C 레지스터에서 보정 데이터(calibration data)를 먼저 읽은 뒤, 보상(compensate) 전 온도값과 압력값을 읽어 보정 데이터를 토대로 복잡한 수식을 거쳐야 원하는 압력값을 얻을 수 있기 떄문에 코드가 길다...

어찌저찌 동작하게는 만들었는데, I2C R/W 기능을 추상화하지 못해 코드가 좀 지저분한게 단점 ㅋㅋ (어차피 예제 프로젝트라 코드를 고도화할 생각은 없었지만..)

#include "bmp180.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <inttypes.h>
#include <math.h>

#define BMP180_I2C_ADDR         0x77    /**< BMP180 I2C address */

#define BMP180_CAL_AC1          0xAA    /**< R Calibration data (16 bits) */
#define BMP180_CAL_AC2          0xAC    /**< R Calibration data (16 bits) */
#define BMP180_CAL_AC3          0xAE    /**< R Calibration data (16 bits) */
#define BMP180_CAL_AC4          0xB0    /**< R Calibration data (16 bits) */
#define BMP180_CAL_AC5          0xB2    /**< R Calibration data (16 bits) */
#define BMP180_CAL_AC6          0xB4    /**< R Calibration data (16 bits) */
#define BMP180_CAL_B1           0xB6    /**< R Calibration data (16 bits) */
#define BMP180_CAL_B2           0xB8    /**< R Calibration data (16 bits) */
#define BMP180_CAL_MB           0xBA    /**< R Calibration data (16 bits) */
#define BMP180_CAL_MC           0xBC    /**< R Calibration data (16 bits) */
#define BMP180_CAL_MD           0xBE    /**< R Calibration data (16 bits) */

#define BMP180_CONTROL          0xF4    /**< Control register */
#define BMP180_TEMPDATA         0xF6    /**< Temperature data register */
#define BMP180_PRESSUREDATA     0xF6    /**< Pressure data register */
#define BMP180_READTEMPCMD      0x2E    /**< Read temperature control register value */
#define BMP180_READPRESSURECMD  0x34    /**< Read pressure control register value */

typedef enum {
    ULTRA_LOW_POWER = 0,
    STANDARD = 1,
    HIGH_RESOLUTION = 2,
    ULTRA_HIGH_RESOLUTION = 3
} eBMP180Mode;

typedef struct bmp180_cal_data {
    int16_t ac1, ac2, ac3, b1, b2, mb, mc, md;
    uint16_t ac4, ac5, ac6;

    bmp180_cal_data() {
        ac1 = ac2 = ac3 = ac4 = ac5 = ac6 = b1 = b2 = mb = mc = md = 0;
    };
} bmp180_cal_data_t;

CBmp180Ctrl::CBmp180Ctrl()
{
    m_i2c_master = nullptr;
    m_cal_data = bmp180_cal_data_t();
}

bool CBmp180Ctrl::initialize(CI2CMaster *i2c_master)
{
    m_i2c_master = i2c_master;

    uint8_t chip_id = 0;
    if (!read_chip_id(&chip_id)) {
        return false;
    }
    if (chip_id != 0x55) {
        return false;
    }

    if (!read_calibration_data()) {
        return false;
    }

    return true;
}

bool CBmp180Ctrl::read_measurement(float *pressure, eBMP180Mode mode/*=eBMP180Mode::ULTRA_HIGH_RESOLUTION*/)
{
    int32_t raw_temperature = 0;
    int32_t raw_pressure = 0;
    if (!read_uncompensated_temperature(&raw_temperature))
        return false;
    if (!read_uncompensated_pressure(mode, &raw_pressure))
        return false;

    // unit: Pa = 0.01hPa = 0.001kPa
    int32_t conv_val = calculate_true_pressure(raw_temperature, raw_pressure, mode);
    if (pressure)
        *pressure = (float)conv_val;
    
    return true;
}

bool CBmp180Ctrl::read_calibration_data()
{
    uint8_t data_write[1] = {0, };
    uint8_t data_read[2] = {0, };
    data_write[0] = BMP180_CAL_AC1;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.ac1 = (((int16_t)data_read[0]) << 8) | ((int16_t)data_read[1]);

    data_write[0] = BMP180_CAL_AC2;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.ac2 = (((int16_t)data_read[0]) << 8) | ((int16_t)data_read[1]);

    data_write[0] = BMP180_CAL_AC3;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.ac3 = (((int16_t)data_read[0] << 8)) | ((int16_t)data_read[1]);

    data_write[0] = BMP180_CAL_AC4;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.ac4 = (((uint16_t)data_read[0] << 8)) | ((uint16_t)data_read[1]);

    data_write[0] = BMP180_CAL_AC5;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.ac5 = (((uint16_t)data_read[0] << 8)) | ((uint16_t)data_read[1]);

    data_write[0] = BMP180_CAL_AC6;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.ac6 = (((uint16_t)data_read[0] << 8)) | ((uint16_t)data_read[1]);

    data_write[0] = BMP180_CAL_B1;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.b1 = (((int16_t)data_read[0] << 8)) | ((int16_t)data_read[1]);

    data_write[0] = BMP180_CAL_B2;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.b2 = (((int16_t)data_read[0] << 8)) | ((int16_t)data_read[1]);

    data_write[0] = BMP180_CAL_MB;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.mb = (((int16_t)data_read[0] << 8)) | ((int16_t)data_read[1]);

    data_write[0] = BMP180_CAL_MC;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.mc = (((int16_t)data_read[0] << 8)) | ((int16_t)data_read[1]);

    data_write[0] = BMP180_CAL_MD;
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write), data_read, sizeof(data_read)))
        return false;
    m_cal_data.md = (((int16_t)data_read[0] << 8)) | ((int16_t)data_read[1]);

    return true;
}

bool CBmp180Ctrl::read_uncompensated_temperature(int32_t *temperature)
{
    uint8_t data_write[2] = {BMP180_CONTROL, BMP180_READTEMPCMD};
    
    if (!m_i2c_master->write_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write)))
        return false;
    vTaskDelay(pdMS_TO_TICKS(100));

    data_write[0] = BMP180_TEMPDATA;
    uint8_t data_read[2] = {0, };
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, 1, data_read, sizeof(data_read)))
        return false;

    if (temperature) {
        *temperature = (((int32_t)data_read[0]) << 8) | ((int32_t)data_read[1]);
    }

    return true;
}

bool CBmp180Ctrl::read_uncompensated_pressure(eBMP180Mode mode, int32_t *pressure)
{
    int32_t raw_pressure;
    uint8_t data_write[2] = {BMP180_CONTROL, (uint8_t)(BMP180_READPRESSURECMD + ((int)mode << 6))};
    if (!m_i2c_master->write_bytes(BMP180_I2C_ADDR, data_write, sizeof(data_write)))
        return false;
    
    vTaskDelay(pdMS_TO_TICKS(100));

    data_write[0] = BMP180_PRESSUREDATA;
    uint8_t data_read[3] = {0, };
    if (!m_i2c_master->write_and_read_bytes(BMP180_I2C_ADDR, data_write, 1, data_read, sizeof(data_read)))
        return false;
    raw_pressure = (((int32_t)data_read[0]) << 16) | (((int32_t)data_read[1]) << 8) | ((int32_t)data_read[0]);
    raw_pressure = raw_pressure >> (8 - (int)mode);
    
    if (pressure) {
        *pressure = raw_pressure;
    }
    
    return true;
}

int32_t CBmp180Ctrl::calculate_value_b5(int32_t value_ut)
{
    int32_t X1 = (value_ut - (int32_t)m_cal_data.ac6) * ((int32_t)m_cal_data.ac5) >> 15;
    int32_t X2 = (((int32_t)m_cal_data.mc) << 11) / (X1 + (int32_t)m_cal_data.md);

    return X1 + X2;
}

int32_t CBmp180Ctrl::calculate_true_pressure(int32_t raw_temperature, int32_t raw_pressure, eBMP180Mode mode)
{
    int32_t UT, UP, B3, B5, B6, X1, X2, X3, p;
    uint32_t B4, B7;
    int32_t result;

    UT = raw_temperature;
    UP = raw_pressure;

    B5 = calculate_value_b5(UT);
    B6 = B5 - 4000;
    
    X1 = ((int32_t)m_cal_data.b2 * ((B6 * B6) >> 12)) >> 11;
    X2 = ((int32_t)m_cal_data.ac2 * B6) >> 11;
    X3 = X1 + X2;
    B3 = ((((int32_t)m_cal_data.ac1 * 4 + X3) << (int)mode) + 2) / 4;
    
    X1 = ((int32_t)m_cal_data.ac3 * B6) >> 13;
    X2 = ((int32_t)m_cal_data.b1 * ((B6 * B6) >> 12)) >> 16;
    X3 = ((X1 + X2) + 2) >> 2;
    B4 = ((uint32_t)m_cal_data.ac4 * (uint32_t)(X3 + 32768)) >> 15;
    B7 = ((uint32_t)UP - B3) * (uint32_t)(50000UL >> (int)mode);

    p = B7 < 0x80000000UL ? (B7 * 2) / B4 : (B7 / B4) * 2;
    X1 = (p >> 8) * (p >> 8);
    X1 = (X1 * 3038) >> 16;
    X2 = (-7357 * p) >> 16;
    
    result = p + ((X1 + X2 + 3791) >> 4);
    return result;
}

2.5. 압력 센서 엔드포인트 생성

Pressure Measurement 디바이스 타입에 대한 엔드포인트는 esp-matter sdk를 통해 쉽게 추가할 수 있다

기압계 엔트포인트 클래스는 알아보기 쉽게 CBarometer로 작명했다

bool CBarometer::matter_init_endpoint()
{
    esp_matter::node_t *root = GetSystem()->get_root_node();
    esp_matter::endpoint::pressure_sensor::config_t config_endpoint;
    uint8_t flags = esp_matter::ENDPOINT_FLAG_DESTROYABLE;
    m_endpoint = esp_matter::endpoint::pressure_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();
}

MeasuredValue, MinMeasuredValue, MaxMeasuredValue 어트리뷰트가 자동으로 추가되므로 필수 구현 어트리뷰트에 대해서는 크게 고민할 필요가 없으며, Min/Max value는 센서 모듈 스펙과 맞추기 위해 어트리뷰트 값 변경 메서드를 구현해줬다

bool CBarometer::set_pressure_measurement_min_measured_value(int16_t value)
{
    esp_matter::cluster_t *cluster = esp_matter::cluster::get(m_endpoint, chip::app::Clusters::PressureMeasurement::Id);
    if (!cluster) {
        return false;
    }
    esp_matter::attribute_t *attribute = esp_matter::attribute::get(cluster, chip::app::Clusters::PressureMeasurement::Attributes::MinMeasuredValue::Id);
    if (!attribute) {
        return false;
    }
    esp_matter_attr_val_t val = esp_matter_nullable_int16(value);
    esp_err_t ret = esp_matter::attribute::set_val(attribute, &val);
    if (ret != ESP_OK) {
        return false;
    }

    return true;
}

bool CBarometer::set_pressure_measurement_max_measured_value(int16_t value)
{
    esp_matter::cluster_t *cluster = esp_matter::cluster::get(m_endpoint, chip::app::Clusters::PressureMeasurement::Id);
    if (!cluster) {
        return false;
    }
    esp_matter::attribute_t *attribute = esp_matter::attribute::get(cluster, chip::app::Clusters::PressureMeasurement::Attributes::MaxMeasuredValue::Id);
    if (!attribute) {
        return false;
    }
    esp_matter_attr_val_t val = esp_matter_nullable_int16(value);
    esp_err_t ret = esp_matter::attribute::set_val(attribute, &val);
    if (ret != ESP_OK) {
        return false;
    }

    return true;
}

ESP32 부팅 시에 해당 메서드를 호출하면 된다

#include "barometer.h"

bool CSystem::initialize()
{
    CBarometer *sensor = new CBarometer();
    if (sensor && sensor->matter_init_endpoint()) {
        m_device_list.push_back(sensor);
        sensor->set_pressure_measurement_min_measured_value(300); // 300hPa
        sensor->set_pressure_measurement_max_measured_value(1100); // 1100hPa
    } else {
        return false;
    }
    
    return true;
}

 

유념해야할 점은, Matter의 압력 센서는 kPa (킬로파스칼) 단위를 사용하는데, MeasuredValue 어트리뷰트는 해당 값에 10을 곱한 값을 넣어야 한다는 점이다

정리하면, 1kPa = 1000Pa = 10hPa(헥스파스칼)이므로, 만약 압력 센서 측정값의 단위가 Pa이라면 해당값을 100으로 나눈 값을 어트리뷰트에 쓰면 되며, 만약 측정 단위가 hPa이라면 값을 그대로 쓰면 된다는 의미~

 

BMP180의 측정 단위는 Pa이므로 다음과 같이 구현하면 된다

void CBarometer::update_measured_value_pressure(float value)
{
    m_measured_value_pressure = (int16_t)(value / 100.f);   // Pa -> hPa
    if (m_measured_value_pressure != m_measured_value_pressure_prev) {
        matter_update_clus_pressuremeasure_attr_measureval();
    }
    m_measured_value_pressure_prev = m_measured_value_pressure;
}

void CBarometer::matter_update_clus_pressuremeasure_attr_measureval(bool force_update/*=false*/)
{
    esp_matter_attr_val_t target_value = esp_matter_nullable_int16(m_measured_value_pressure);
    matter_update_cluster_attribute_common(
        m_endpoint_id,
        chip::app::Clusters::PressureMeasurement::Id,
        chip::app::Clusters::PressureMeasurement::Attributes::MeasuredValue::Id,
        target_value,
        &m_matter_update_by_client_clus_pressuremeasure_attr_measureval,
        force_update
    );
}

2.6. Measurement Task 생성

#include "system.h"
#include "bmp180.h"
#include "barometer.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;
    CDevice * dev;
    float pressure = 0.f;

    while (1) {
        current_tick_us = esp_timer_get_time();
        if (current_tick_us - last_tick_us >= MEASURE_PERIOD_US) {
            if (GetBmp180Ctrl()->read_measurement(&pressure)) {  // unit: Pa
                dev = obj->find_device_by_endpoint_id(1);
                if (dev) {
                    dev->update_measured_value_pressure(pressure);
                }
                double altitude = GetBmp180Ctrl()->calculate_absolute_altutide((int32_t)pressure);
                GetLogger(eLogType::Info)->Log("Pressure: %g Pa (Altitude: %g m)", pressure, altitude);
            }
            last_tick_us = current_tick_us;
        }
        vTaskDelay(pdMS_TO_TICKS(50));
    }
    vTaskDelete(nullptr);
}

※ 대기압 측정값을 토대로 고도(altitude)도 계산해서 같이 로그로 기록하도록 구현

3. DEMO

3.1. Google Home 액세서리 추가

Apple Home은 iOS 17.3.1 에서 압력 센서 디바이스가 지원되지 않는다... (얘넨 왜 이렇게 Matter 신규 디바이스 지원이 느린거야 -_-)

 

다행히도 Google Home은 지원되길래 커미셔닝을 진행해봤다

Matter 지원 기기 선택 - QR Code 촬영
Matter device commissioning (BLE-WiFi)

 

디바이스 설정

 

액세서리 UI는 단촐하기 그지없다 ㅋㅋ

 

디바이스 세부 사항으로 들어가봐도 그냥 압력값만 kPa 단위로 표시할 뿐이다 ㅎㅎ (사실 센서라는게 측정값을 표시하기만 하면 그만이지 뭐..)

3.2. ESP32 로그


I (4668069) logger: [CBarometer::update_measured_value_pressure] Update measured pressure value as 100551 [barometer.cpp:91]
I (4668069) esp_matter_attribute: ********** W : Endpoint 0x0001's Cluster 0x00000403's Attribute 0x00000000 is 1005 **********
I (4668079) logger: [CSystem::task_timer_function] Pressure: 100551 Pa (Altitude: 64.6393 m) [system.cpp:435]
I (4668079) esp_matter_attribute: ********** R : Endpoint 0x0001's Cluster 0x00000403's Attribute 0x00000000 is 1005 **********
I (4668109) chip[EM]: <<< [E:2651i S:6454 M:204177914] (S) Msg TX to 1:00000000CB36C304 [C4AA] [UDP:[FE80::EEF8:13B8:2EDB:E3E2%st1]:5540] --- Type 0001:05 (IM:ReportData)
I (4668129) chip[EM]: >>> [E:2651i S:6454 M:63159419 (Ack:204177914)] (S) Msg RX from 1:00000000CB36C304 [C4AA] --- Type 0001:01 (IM:StatusResponse)
I (4668139) chip[IM]: Received status response, status is 0x00
I (4668139) chip[EM]: <<< [E:2651i S:6454 M:204177915 (Ack:63159419)] (S) Msg TX to 1:00000000CB36C304 [C4AA] [UDP:[FE80::EEF8:13B8:2EDB:E3E2%st1]:5540] --- Type 0000:10 (SecureChannel:StandaloneAck)
I (4678089) logger: [CBarometer::update_measured_value_pressure] Update measured pressure value as 100456 [barometer.cpp:91]
I (4678089) esp_matter_attribute: ********** W : Endpoint 0x0001's Cluster 0x00000403's Attribute 0x00000000 is 1004 **********
I (4678099) logger: [CSystem::task_timer_function] Pressure: 100456 Pa (Altitude: 72.6008 m) [system.cpp:435]
I (4678099) esp_matter_attribute: ********** R : Endpoint 0x0001's Cluster 0x00000403's Attribute 0x00000000 is 1004 **********
I (4678129) chip[EM]: <<< [E:2652i S:6454 M:204177916] (S) Msg TX to 1:00000000CB36C304 [C4AA] [UDP:[FE80::EEF8:13B8:2EDB:E3E2%st1]:5540] --- Type 0001:05 (IM:ReportData)
I (4678159) chip[EM]: >>> [E:2652i S:6454 M:63159420 (Ack:204177916)] (S) Msg RX from 1:00000000CB36C304 [C4AA] --- Type 0001:01 (IM:StatusResponse)
I (4678169) chip[IM]: Received status response, status is 0x00
I (4678169) chip[EM]: <<< [E:2652i S:6454 M:204177917 (Ack:63159420)] (S) Msg TX to 1:00000000CB36C304 [C4AA] [UDP:[FE80::EEF8:13B8:2EDB:E3E2%st1]:5540] --- Type 0000:10 (SecureChannel:StandaloneAck)



흠... 다음엔 어떤 센서 모듈을 Matter 기기로 만들어볼까나...

아직 창고에는 이것저것 써먹을 수 있는 모듈이 많을텐데, 이번 주말에 시간내서 보물찾기를 좀 해봐야겠다 ㅎㅎ

반응형