아두이노 온습도 센서에서 수집된 데이터로 대시보드를 구성하는 과정을 살펴보겠습니다
총 7개의 포스트로 이루어지며 아래 글목록을 보시면 확인이 가능합니다.
- 시스템 개요
- [Mosquitto] MQTT 프로토콜로 데이터를 발행 및 구독하기 위한 MQTT broker 를 구성합니다.
- [Arduino] 아두이노 보드에 온습도 센서를 연결해서 MQTT 프로토콜로 발행하는 코드를 작성합니다.
- (Docker-compose) [Influxdb+Telegraf+Grafana] 대시보드 구성을 위한 시스템은 도커 컴포즈로 일괄 관리합니다.
- [Influxdb] 온습도 데이터를 저장할 데이터베이스를 구성합니다.
- [Telegraf] MQTT broker 의 데이터를 수신해서 Influx DB 에 저장할 Agent 를 구성합니다.
- [Grafana] 데이터베이스에 저장된 데이터를 사용해서 대시보드를 구성합니다.
하드웨어 정보
개인적으로 사용할 용도라 최소한의 가성비를 고려해서 선택하게 되었습니다.
구매 정보
보드를 먼저 선택한 상태에서 나머지는 동일한 스토어에서 적당한 상품으로 구매하게 되었습니다.
보드 구매 페이지 : https://smartstore.naver.com/3dfuns/products/6122841077
ESP8266 WeMos D1 Mini (C 타입)
블루투스 없이 WIFI 만 사용 가능한 소형 보드 중에 선택하였습니다.
미니 브레드 보드
크기가 굳이 클 필요가 없어 소형으로 사용했습니다.
온습도 센서 AHT10
온도와 습도를 모두 측정할 수 있는 센서 중에 선택하였습니다.
LED + 저항
며칠 사용하다 보니 제대로 동작하는지 알고 싶어 지는 상황이 있어
문제가 되는 경우 표시하기 위한 용도로 붉은색 LED 를 선택했습니다.
5V 2A 어댑터 + C 타입 케이블
보드가 입력 전압이 5V 라서 집에서 굴러 다니던 구형 스마트폰 어댑터를 사용하게 되었습니다.
케이블은 짧지만 연결상태 표시가 되는 것으로 구매하였습니다.
아두이노 코드
1분 단위로 온도와 습도 데이터를 측정해서 MQTT broker 로 전송합니다.
아두이노 개발이 처음이다 보니 아직은 불필요한 로그 및 기능도 포함하고 있습니다.
RoomNum 의 경우 방 위치를 구분하는 용도로 사용합니다.
TopicPrefix 과 같이 수정해서 방 위치를 구분합니다.
중괄호({{}}) 부분은 수정해서 사용해야 합니다.
// 온습도 센서(AHT10), LED 조합
// 데이터를 Mqtt 로 전송하고
// WIFI 또는 온습도 센서의 상태 및 측정된 온습도의 값에 따라 문제가 있는 경우 LED ON
// 방 번호에 따라 같이 수정 필요
const char* RoomNum = "R1";
const char* TopicPrefix = "DRS/R1/";
const char* TopicSep = "/";
#include <TimeLib.h>
#include <TimeAlarms.h>
bool logMqtt = true; // 로그 유형(콘솔 or Mqtt)
#pragma region Wifi & MQTT
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
const char* ssid = "{{SSID}}";
const char* password = "{{PWD}}";
const char* mqtt_server = "{{SERVER}}";
const unsigned int mqtt_port = {{PORT}};
const char* mqtt_user = "{{USER}}";
const char* mqtt_pwd = "{{PWD}}";
#pragma region Command
const char* TopicCommand = "CMD";
//const char* TopicCmdReset = "RST";
const char* TopicCmdAng = "ANG"; // 모터 각도
const char* TopicCmdLog = "LOG"; // LOG
#pragma endregion comment
const char* TopicLog = "LOG"; // For Log
const char* TopicWifi = "WIFI";
const char* TopicLed = "LED";
const char* TopicFreeHeap = "HEAP";
const char* TopicAht = "AHT";
const char* TopicTemp = "TEMP";
const char* TopicHumidity = "HUM";
WiFiClient espClient;
PubSubClient client(espClient);
const unsigned long delayWifi = 1000 * 5; // 5초
unsigned long prevWifi = 0;
void connectMqtt(bool checkDelay = true) {
if (checkDelay) {
if (millis() - prevWifi < delayWifi) {
return;
}
}
Serial.print("Attempting MQTT connection...");
// Create a random client ID
String clientId = String("AHT") + RoomNum + String(random(0xffff), HEX);
// Attempt to connect
if (client.connect(clientId.c_str(), mqtt_user, mqtt_pwd)) {
Serial.println("connected");
// ... and resubscribe
String topic = String(TopicPrefix) + TopicCommand + TopicSep + "#";
client.subscribe(topic.c_str());
String wifiTopic = String(TopicPrefix) + TopicWifi;
client.publish(wifiTopic.c_str(), String(HIGH).c_str(), true);
prevWifi = millis();
}
else {
Serial.print("failed, rc=");
Serial.print(client.state());
}
}
void setup_wifi() {
// We start by connecting to a WiFi network
Serial.println("");
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
randomSeed(micros());
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
client.setServer(mqtt_server, mqtt_port);
client.setCallback(mqttCallback);
connectMqtt(false);
}
int callbackAngle = -1;
const int maxTopicParts = 10; // 배열의 최대 크기
char* topicParts[maxTopicParts]; // 문자열을 저장할 배열
void mqttCallback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
char payloadStr[length + 1];
for (int i = 0; i < length; i++) {
payloadStr[i] = (char)payload[i];
}
payloadStr[length] = '\0'; // Null-terminate the char array
Serial.println(payloadStr);
// Topic 을 '/' 단위로 문자열 구분하여 배열에 저장
char topicStr[strlen(topic) + 1];
strcpy(topicStr, topic);
int startIndex = 0;
int endIndex = 0;
int partIndex = 0;
while (topicStr[endIndex] != '\0' && partIndex < maxTopicParts) {
if (topicStr[endIndex] == '/') {
topicStr[endIndex] = '\0';
topicParts[partIndex++] = &topicStr[startIndex];
startIndex = endIndex + 1;
}
endIndex++;
}
if (partIndex < maxTopicParts) {
topicParts[partIndex] = &topicStr[startIndex];
}
if (strcmp(topicParts[2], TopicCmdAng) == 0) {
int ang = abs(atoi(payloadStr));
if (ang > 180) ang = 180;
callbackAngle = ang;
}
else if (strcmp(topicParts[2], TopicCmdLog) == 0) {
logMqtt = !logMqtt;
}
}
void CustomLog(const char* msg) {
if (logMqtt) {
if (client.connected()) {
client.publish((String(TopicPrefix) + TopicLog).c_str(), msg);
}
}
Serial.println(msg);
}
#pragma endregion comment
#pragma region 온/습도
#include <Adafruit_AHTX0.h> // AHTX0 라이브러리 포함
Adafruit_AHTX0 aht; // aht 객체 생성
void setup_AHT() {
int tryCount = 0;
Serial.print("Try AHT Connection .");
bool isAhtOn = false;
isAhtOn = aht.begin();
while (!isAhtOn) {
Serial.print("°");
delay(300);
isAhtOn = aht.getStatus() != 0xFF;
if (isAhtOn)
continue;
tryCount++;
if (tryCount > 9) {
break;
}
}
Serial.println();
bool isAhtOn = aht.getStatus() != 0xFF;
Serial.println(isAhtOn ? "온/습도 센서에 연결되었습니다." : "온/습도 센서에 연결되지 않았습니다.");
if (client.connected()) {
client.publish((String(TopicPrefix) + TopicAht).c_str(), String(isAhtOn ? HIGH : LOW).c_str(), true);
}
}
float tempLast = 0;
float humiLast = 0;
void loop_AHT() {
bool isAhtOn = aht.getStatus() != 0xFF;
if (!isAhtOn) {
setup_AHT();
return;
}
sensors_event_t humi, temp; // sensors_event_t 타입의 temp와 humidity 변수 선언
aht.getEvent(&humi, &temp); // 온도, 습도 데이터 갱신
float tempNow = temp.temperature;
float humiNow = humi.relative_humidity;
tempLast = tempNow;
humiLast = humiNow;
CustomLog((String("온도 : ") + tempNow + " | 습도 : " + humiNow).c_str());
if (client.connected()) {
client.publish((String(TopicPrefix) + TopicTemp).c_str(), String(tempNow).c_str());
client.publish((String(TopicPrefix) + TopicHumidity).c_str(), String(humiNow).c_str());
}
}
#pragma endregion comment
#pragma region CheckFreeHeap
void loop_CheckFreeHeap() {
uint32_t freeHeap = ESP.getFreeHeap();
Serial.print("Free Heap: ");
Serial.println(freeHeap);
if (client.connected()) {
char freeHeapStr[10];
itoa(freeHeap, freeHeapStr, 10); // freeHeap 값을 문자열로 변환
client.publish((String(TopicPrefix) + TopicFreeHeap).c_str(), freeHeapStr);
}
}
#pragma endregion comment
#pragma region LED
#define LED_PIN 14 // LED PIN
void setup_Led() {
pinMode(LED_PIN, OUTPUT);
}
bool ledStatus = true;
void loop_Led() {
const char* errMsg = "";
// 온습도 센서가 꺼져 있거나 WIFI 가 연결되지 않은 경우 불이 켜짐
bool status = client.connected();
if(status) {
status = aht.getStatus() != 0xFF;
if(status) {
// 온도는 0 ~ 45 도 사이
// 습도는 30 ~ 85
status = (tempLast > 0 && tempLast < 45) && (humiLast > 30 && humiLast < 85);
if(!status) {
errMsg = "AHT data invalid";
}
} else {
errMsg = "AHT OFF";
}
} else {
errMsg = "MQTT OFF";
}
if(ledStatus != status) {
digitalWrite(LED_PIN, !status ? HIGH : LOW);
if (client.connected()) {
client.publish((String(TopicPrefix) + TopicLed).c_str(), String(status ? HIGH : LOW).c_str(), true);
}
CustomLog((String(!status ? "LED ON : " : "LED OFF") + errMsg).c_str());
}
ledStatus = status;
}
#pragma endregion comment
#pragma region Default
void setup() {
Serial.begin(115200);
setTime(8, 29, 0, 1, 1, 11); // set time to Saturday 8:29:00am Jan 1 2011
setup_Led();
setup_wifi();
setup_AHT();
loop_Led();
Alarm.timerRepeat(1, loop_Led);
loop_AHT();
Alarm.timerRepeat(60, loop_AHT);
loop_CheckFreeHeap();
Alarm.timerRepeat(60, loop_CheckFreeHeap);
}
void loop() {
if (!client.connected()) {
connectMqtt();
}
if (client.connected()) {
client.loop();
}
loopCheck();
Alarm.delay(15);
}
const unsigned long delayLoopCheck = 1000 * 60;
unsigned long prevLoopCheck = 0;
void loopCheck() {
if (millis() - prevLoopCheck < delayLoopCheck) return;
CustomLog((String("LoopCheck : ") + millis()).c_str());
prevLoopCheck = millis();
}
#pragma endregion comment
0 Comments