지난번 글에서 주방환풍기를 AMG8833 8x8 적외선 온도 센서로 작동시킨 적이 있는데, 처음부터 불량 섹터들이 있었는데 부품이 결국 고장이 났습니다(주의: AMG8833은 전원으로 3V를 써야 합니다. 과거 불량들은 실수로 5V에 연결해서 고장났었던 것일 수도 있어요).
이번에는 조금 다른 부품을 구입했는데 GY-MCU8833으로 AMG8833처럼 I2C가 아닌 시리얼 통신 방식을 지원합니다. 그러다보니 그냥 가져다 쓸 수 있는 소스가 없었습니다.
그래서, 프로토콜을 알아보려고 했는데, 구글과 바이두를 뒤져도 찾을 수가 없었고 시리얼데이터를 보니까 간단한 포맷인 것 같아서 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개 받은 후 다른 종류로 구입해 보았습니다.
'홈어시스턴트 IoT' 카테고리의 다른 글
지그비 리모트 4버튼 스위치 - 홈어시스턴트와 Sonoff 지그비 동글 (0) | 2023.08.22 |
---|---|
미세먼지와 이산화탄소에 진심? Waveshare e-ink에 항상 표시 (0) | 2023.08.02 |
구형에어컨 제습운전 자동화 홈어시스턴트 ESPHome DIY 적외선리모콘 (0) | 2023.07.22 |
가정 내 소비전력 모니터링 - 시하스 PMM-300-Z 지그비 (0) | 2023.07.05 |
Xiaomi Gateway 3 최신 펌웨어(v1.5.5.xxx) 홈어시스턴트 통합 주의 (0) | 2023.06.25 |