본문 바로가기

홈어시스턴트 IoT

8x8 적외선 온도계 GY-MCU8833의 ESPHome 커스텀 센서 연동

지난번 글에서 주방환풍기를 AMG8833 8x8 적외선 온도 센서로 작동시킨 적이 있는데, 처음부터 불량 섹터들이 있었는데 부품이 결국 고장이 났습니다(주의: AMG8833은 전원으로 3V를 써야 합니다. 과거 불량들은 실수로 5V에 연결해서 고장났었던 것일 수도 있어요). 

 

이번에는 조금 다른 부품을 구입했는데 GY-MCU8833으로 AMG8833처럼 I2C가 아닌 시리얼 통신 방식을 지원합니다. 그러다보니 그냥 가져다 쓸 수 있는 소스가 없었습니다. 

 

GY-MCU8833

 

그래서, 프로토콜을 알아보려고 했는데, 구글과 바이두를 뒤져도 찾을 수가 없었고 시리얼데이터를 보니까 간단한 포맷인 것 같아서 ESPHome 커스텀 센서로 구현해 보았습니다. 

 

[데이터형식] 

  • 6개 바이트 : 헤더(불변 5바이트, 가변 1바이트)  
  • 64개 바이트 : 바디(byte자료로 64개 센서의 온도 값) 
  • 1개 바이트 : 테일(8비트 체크섬)  
 

다만 체크섬 등이 있을텐데 파악이 안되어서 일단 무시하고 바디 정보만 활용하였습니다. -> 체크섬 추가 완료(7/26)

[홈어시스턴트 연동 방법] 

ESPHome에서 .H파일을 하나 만들어서 간단하게 아두이노 코드로 짜면서 Sensor()를 만든 후, publish_state()를 통하여 값을 넣어주면 YAML에서 람다 함수에서 커스텀 센서를 등록하여 연동하는 방법인 것 같습니다. 

 

아래 YAML 코드는 AMG8833의 ESPHome 코드를 공개한 분의 것을 참고하였습니다. 일단 저는 최고 온도만 있으면 되기 때문에 열화상이미지 등은 구현하지 않았습니다. 

sensor:
  - platform: custom
    lambda: |-
      auto mcu8833 = new MCU8833Component(id(uart_bus));
      App.register_component(mcu8833);
      return {mcu8833->max_temperature, mcu8833->min_temperature, mcu8833->avg_temperature, mcu8833->min_index, mcu8833->max_index};

특이한 점은 커스텀 센서로 아두이노 코드를 짜는 경우 초당 60회 정도 loop() 함수를 부르게 되는데, 홈어시스턴트로 초당 60회의 센서 업데이트가 전달되는 문제가 있다는 것입니다. 그것을 피하기 위해서는 PollingComponent를 상속해서 loop()대신 update()를 이용합니다. 

 

아래 .H 코드도 같은 코드를 참고... 아래와 같이 5000ms(5초)마다 update()를 호출하게 됩니다. 또한 아두이노가 커스텀 시리얼포트를 사용하기 위한 코드이기도 합니다. 위쪽 코드의 id(uart_bus)가 그것입니다. YAML에 uart:를 GPIO에 매핑해 줍니다. 

class MCU8833Component : public PollingComponent, public UARTDevice {
 public:
  MCU8833Component(UARTComponent *parent) : PollingComponent(5000), UARTDevice(parent) {}

  Sensor *max_temperature = new Sensor();
  Sensor *min_temperature = new Sensor();
  Sensor *avg_temperature = new Sensor();
  Sensor *min_index = new Sensor();
  Sensor *max_index = new Sensor();

update()를 5초마다 실행하게 되면 시리얼데이터 디코딩은 한번 호출되었을 때 모두 처리해야 합니다. 

 

아두이노 코드를 쓰라고 하지만 사실 시리얼포트를 다루는 함수가 조금 다릅니다. 예를 들면, Serial.readBytes() 함수는 없고 대신 UARTDevice에서 상속된 함수 read_byte()는 1바이트만 읽습니다. 

 

어쨌든 mcu8833.h는 다음과 같습니다. 겨우 작동하는 수준이라서 신뢰성은 없음에 유의해 주세요. 

 

#include "esphome.h"

class MCU8833Component : public PollingComponent, public UARTDevice {
 public:
  MCU8833Component(UARTComponent *parent) : PollingComponent(5000), UARTDevice(parent) {}

  Sensor *max_temperature = new Sensor();
  Sensor *min_temperature = new Sensor();
  Sensor *avg_temperature = new Sensor();
  Sensor *min_index = new Sensor();
  Sensor *max_index = new Sensor();

  // GY-MCU8833 serial protocol unknown 
  // HEADER = 6 bytes, BODY = 64 bytes, TAIL = 1 byte(checksum of 0 ~ 69th bytes)  
  const byte PACKET_LENGTH = 71;  
  const byte NUM_CELL = 64; 
  const byte START_BYTES[2] = {0xA4, 0x03};
  const int MAX_BUFFER = 512; 

  void setup() override 
  {
    // nothing to do here
  }

  // called 60 times per second
  void loop() override
  {
    // nothing to do here 
  }

  // for PollingComponent 
  void update() override
  {
    bool read_success = false; 
    byte packet[MAX_BUFFER];   
    byte read_index = 0; 

    while (available()) 
    {
      byte ch; 
      read_success = read_byte(&ch); 
      if (!read_success)
      {
        break; 
      }
      else
      {
        packet[read_index++] = ch; 
      }
      if (read_index > MAX_BUFFER - 1)
      { 
        break; 
      }
    }

    if(read_index >= PACKET_LENGTH)
    {
      // Variables to store calculations
      int sumTemp = 0;
      float maxTemp = -127; // Minimum possible 8-bit integer value
      float minTemp = 127;  // Maximum possible 8-bit integer value
      int maxIndex = -1; 
      int minIndex = -1; 
      byte packet_selected[PACKET_LENGTH]; 

        // Find keyword 
      int keyword = 0;
      byte data_start = 0; 
      byte data_index = 0; 
      byte checksum = 0; 

      for (byte i = 0; i < read_index; i++)
      {
        if (keyword == 0 && packet[i] == START_BYTES[0])
        {
          keyword = 1; 
          packet_selected[data_index++] = START_BYTES[0]; 
        }
        else if(keyword == 1 && packet[i] == START_BYTES[1]) 
        {
          keyword = 2; 
          packet_selected[data_index++] = START_BYTES[1]; 
        }
        else if(keyword == 2) 
        {
            if (data_index == (PACKET_LENGTH - 1))
            {
              checksum = packet[i];
              break; 
            }
            else
            {
              packet_selected[data_index++] = packet[i]; 
            }
        }
      }

      // ignore incomplete packet 
      if (data_index != (PACKET_LENGTH - 1)) return; 

      // verify packet : Checksum 8bits modulo 256 
      int checksum_calc = 0; 
      for (byte i = 0; i < PACKET_LENGTH - 1; i++)
      {
        checksum_calc += (int)packet_selected[i];
      }

      byte checksum_result = (byte)(checksum_calc % 256); 
      if (checksum_result != checksum)
      {
          return; 
      }

      // Find maximum, minimum, and sum of temperatures
      for (byte i = 6; i < PACKET_LENGTH - 1; i++) 
      {
        int temperature = packet_selected[i];
        if (temperature > maxTemp) {
          maxTemp = temperature;
          maxIndex = i + 1; 
        }
        if (temperature < minTemp) {
          minTemp = temperature;
          minIndex = i + 1; 
        }
        sumTemp += temperature;
      }

      // Calculate average temperature
      float avgTemp = static_cast<float>(sumTemp) / NUM_CELL;

      max_temperature->publish_state(maxTemp);
      min_temperature->publish_state(minTemp);
      min_index->publish_state(minIndex);
      max_index->publish_state(maxIndex);
      avg_temperature->publish_state(avgTemp);
    }
  }
};

 

mcu8833.yaml은 다음과 같습니다. - platform: homeassistant아래의 내용은 4글자 세븐 세그먼트 LED 디스플레이를 위한 것입니다. 

esphome:
  name: mcu8833
  platform: ESP8266
  board: d1_mini
  includes:
    - mcu8833.h

wifi:
  ssid: !secret ssid
  password: !secret password
  domain: !secret domain
  
#  manual_ip:
#    # Set this to the IP of the ESP
#    static_ip: 192.168.0.84
#    # Set this to the IP address of the router. Often ends with .1
#    gateway: 192.168.0.1
#    # The subnet of the network. 255.255.255.0 works for most home networks.
#    subnet: 255.255.255.0

logger:

# Enable Home Assistant API
api:
  password: ""

ota:
  password: ""
  
captive_portal:

uart:
  id: uart_bus
  tx_pin: 4
  rx_pin: 5
  baud_rate: 9600

sensor:
  - platform: custom
    lambda: |-
      auto mcu8833 = new MCU8833Component(id(uart_bus));
      App.register_component(mcu8833);
      return {mcu8833->max_temperature, mcu8833->min_temperature, mcu8833->avg_temperature, mcu8833->min_index, mcu8833->max_index};

    sensors:
      - name: "Thermal Sensor Max"
        unit_of_measurement: °C
        device_class: temperature
        accuracy_decimals: 2

      - name: "Thermal Sensor Min"
        unit_of_measurement: °C
        device_class: temperature
        accuracy_decimals: 2

      - name: "Thermal Sensor Avg"
        unit_of_measurement: °C
        device_class: temperature
        accuracy_decimals: 2

      - name: "Thermal Sensor Min Index"
        accuracy_decimals: 0

      - name: "Thermal Sensor Max Index"
        accuracy_decimals: 0

  - platform: homeassistant 
    entity_id: sensor.thermal_sensor_max
    id: thermal_max_temp
    internal: true 

# Example configuration entry
display:
    platform: tm1637
    id: tm1637_display
    clk_pin: D5 #GPIO14
    dio_pin: D6 #GPIO12
    inverted: false
    intensity: 2
    length: 4
    lambda: |-
      it.printf(0,"%4.0f", id(thermal_max_temp).state);
 

 

아래는 홈어시스턴트에 등록된 후 대시보드의 모습입니다. 

 

 

 

끝으로 아두이노에서 테스트한 막~짠 코드는 다음과 같습니다(일부는 ChatGPT 3.5가 작성). 이 때는 0xA4 0x03을 키워드로 보지 않고 작성했으니 참고 바랍니다. 

#include <SoftwareSerial.h>

const byte PACKET_LENGTH = 69;
const byte START_BYTES[] = {0x03, 0x09};
const byte DATA_START_INDEX = 3;
const byte DATA_END_INDEX = 66;
const byte NUM_DATA_POINTS = DATA_END_INDEX - DATA_START_INDEX + 1;

SoftwareSerial SSerial(5,4); // rx, tx

void setup() 
{
  Serial.begin(115200); 
  SSerial.begin(9600);
  Serial.print("TEST");
}

bool keyword = false; 

void ReadPacket()
{
  byte packet[PACKET_LENGTH];  
  int nBytes = SSerial.readBytes(packet, PACKET_LENGTH);

  if (nBytes >= PACKET_LENGTH) 
  {
    // Variables to store calculations
    int maxTemp = -127; // Minimum possible 8-bit integer value
    int minTemp = 127;  // Maximum possible 8-bit integer value
    int maxIndex = -1; 
    int minIndex = -1; 
    int sumTemp = 0;

    // Find maximum, minimum, and sum of temperatures
    for (byte i = DATA_START_INDEX; i <= DATA_END_INDEX; i++) 
    {
      int temperature = packet[i];
      if (temperature > maxTemp) {
        maxTemp = temperature;
        maxIndex = i - DATA_START_INDEX; 
      }
      if (temperature < minTemp) {
        minTemp = temperature;
        minIndex = i - DATA_START_INDEX; 
      }
      sumTemp += temperature;
    }

    // Calculate average temperature
    float avgTemp = static_cast<float>(sumTemp) / NUM_DATA_POINTS;

    // Output results
    Serial.print("Max Temperature: ");
    Serial.print(maxTemp);
    Serial.print(" (Index: ");
    Serial.print(maxIndex);
    Serial.println(")");

    Serial.print("Min Temperature: ");
    Serial.print(minTemp);
    Serial.print(" (Index: ");
    Serial.print(minIndex);
    Serial.println(")");

    Serial.print("Average Temperature: ");
    Serial.println(avgTemp);      
  }
}

void loop() 
{
  if (SSerial.available() > 0) 
  {
    byte ch = SSerial.read();
    if (keyword == false && ch == START_BYTES[0])
    {
      keyword = true; 
    }
    else if(keyword == true && ch == START_BYTES[1])
    {
      ReadPacket(); 
      keyword = false; 
    }
    else
    {
      keyword = false; 
    }
  }
}
 

구입한 제품입니다. AMG8833 정품보다는 절반 정도로 저렴하고, AMG8833 클론들보다는 조금 비싼 것 같아요. 지난번에 불량(제조불량으로 의심되는) 2개 받은 후 다른 종류로 구입해 보았습니다.  

 

 

 

홈어시스턴트 화면(습도는 별도 아카라 센서)

 
 
 
 

주방팬 후드 환기 자동화를 위해 인덕션 위에 설치