2021-06-03 update log: 캠핑장 실 사용 후기 추가
ESP8266 모듈을 활용한 새로운 프로젝트 캠핑용 온풍기를 온도조절이 가능하도록 하는 스마트 플러그를 만들어볼까 합니다.
지난번에 캠핑용 온풍기를 리뷰한 적이 있었는데 이 온풍기의 단점은 전원 스위치 외에는 아무것도 없어서 주변 온도에 관계없이 전원이 켜져 있으면 따뜻한 바람이 계속 나온다는 겁니다. 온풍기를 사용하면 공기가 건조해지기 때문에 어느 정도 공기가 따뜻해지면 온풍기를 끄고 싶은데 그럴 수 없어서, 온도에 따라 온풍기의 전원을 제어할 수 있는 방법을 찾아보게 되었습니다.
방법은 아주 간단합니다. 이 온풍기는 단순히 전기가 공급되면 동작하고 차단되면 멈추기 때문에 동작 전원을 릴레이에 연결하고 MCU와 연결해서 미리 설정한 온도 조건에 따라 온도가 높으면 릴레이 끄고 온도가 낮으면 켜는 컨트롤을 하도록 할 계획입니다.
준비물
- ESP8266 모듈(wemos D1 mini)
- 220V AC to 5V DC 변환 모듈
- 릴레이
- 온습도 센서(DHT22)
- 220V 500W 고용량 전원 연결용 케이블
- 점프 케이블
- 220V 플러그, 콘센트 부품들
- 하우징(3D 프린팅)
메인 컨트롤러는 ESP8266 모듈을 사용했습니다. 온도를 설정하고 현제 동작 상태를 확인하려면 LCD 등 부품이 필요하지만 그러면 구성이 복잡해지기 때문에 조작은 와이파이로 연결해서 핸드폰을 통해서 할 생각입니다.
결선도
구성은 220V 콘센트에서 들어오는 전원의 한가닥을 릴레이에 물려서 on/off를 컨트롤할 수 있게 하고 AC-DC 변압 모듈을 이용해서 220V에서 직류 5V 컨트롤러 전원을 땄습니다. 그래서 이 전원으로 ESP8266 모듈, 온습도 모듈, 릴레이 모듈을 동작시킵니다. 컨트롤러는 온습도 모듈에서 읽어온 공기 상태를 바탕으로 릴레이를 켤지 말지를 판단하는 거죠. 로직은 너무 간단한데 제가 골머리를 썩은 건 바로 기계부 제작이었습니다.
기계부 제작
제가 PCB를 디자인해서 DIY 하는 수준이 못돼 나서 이런저런 모듈을 조합하여 사용하다 보니 첫 번째로 패키징이 너무 어려웠습니다. 최대한 작게 만들어야 하고 기성품들과 하우징의 조립도 잘 돼야 하고 그러다 보니 기계부 설계에 시간을 너무 많이 사용했습니다. 그리고 이것도 시행착오가 있어서 2번이나 수정한 모델입니다.
어쨌건 조립이 될 것 같아서 하우징을 출력하고 조립 들어갔습니다.
먼저 모델링한 하우징을 3D 프린터로 뽑습니다. 출력 과정에서도 갑자기 프린터기 노즐이 막혀서 난리가 나는 사태가 있었지만 이참에 프린터기 청소도 하고 우여곡절을 거쳐 출력하였습니다.
다음으로 출력된 하우징에 전기 부분을 담당할 부품입니다. 스마트 플러그의 형태로 플러그와 콘센트가 다 있어야 하기 때문에 시중에 파는 플러그와 콘센트 조립부품을 구매해서 금속 부분만 적출하였습니다.
그리고 위 결선도를 바탕으로 조립했습니다.
최종적으로 온풍기의 500W 전력을 감당해야 하기 때문에 플러그에서 콘센트로 연결되는 케이블은 굵은 2.5SQ를 사용했는데 뻣뻣해서 조립이 쉽지 않았습니다. 그래도 안전이 제일이니까요.
속은 이렇게 지저분해 보이지만 뚜껑 딱 닫으면 감쪽같죠?!
펌웨어
이제 하드웨어가 끝났으니 소프트웨어 작업입니다. ESP8266을 서버처럼 사용하는 방법인 기존 포스팅의 방법과 유사하게 작업 진행했습니다. 생략된 내용은 아래 링크 참고해 주세요.
소스코드입니다. 로직의 설명은 주석으로 대신하겠습니다. 혹시나 관심이 있으신 분이 계시다면 댓글 남겨주시면 내용 추가하도록 하겠습니다. ^^;
소스파일
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ArduinoOTA.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266mDNS.h> // 현재 WIN10, Android에서는 사용이 불가함
#include "DHT.h"
#include "mainPage.html"
#define DHTPIN 5 // GPIO5 = D1, DHT센서 연결
#define HEATPIN 4 // GPIO4 = D2, 히터 컨트롤용 릴레이 연결
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
float h = 0.0; // 상대습도 저장 변수
float t = 0.0; // 섭씨온도 저장 변수
float setTemp = 25.0; // 설정온도 저장 변수
const char *ssid = "ESP8266_AP";
const char *password = "passpass";
unsigned long preMillsForDHT = 0;
unsigned long preMillsForHeat = 0;
ESP8266WiFiMulti wifiMulti;
String strTemp, strHumi, strsetTemp, myLocalIP;
ESP8266WebServer server(80); // server on port 80
IPAddress myIP;
void setup() {
Serial.begin(115200);
dht.begin();
pinMode(HEATPIN, OUTPUT);
digitalWrite(HEATPIN, LOW);
setupCONNECTION(); // WIFI 연결
setupOTA(); // OTA 활성화
setupMAINPAGE(); // 웹서버 초기화
setupMDNS(); // mDNS 활성화
}
void loop() {
ArduinoOTA.handle();
server.handleClient();
delay(0);
getData(); // 변수 t, h 값 갱신
isNeedHeat(setTemp); // 설정온도에 따라 히터 가동 판단
}
// WIFI Connection 부분
void setupCONNECTION() {
WiFi.softAP(ssid, password);
Serial.println('\n');
Serial.print("Access Point \"");
Serial.print(ssid);
Serial.println("\" started");
wifiMulti.addAP("SSID1","password");
wifiMulti.addAP("SSID2","password");
Serial.println('\n');
Serial.println("Connecting...");
while (wifiMulti.run() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println('\n');
Serial.print("Connected to: ");
Serial.println(WiFi.SSID()); // 접속된 AP 표시
Serial.print("IP address:\t");
Serial.println(WiFi.localIP()); // 할당받은 IP주소 표시
}
// 웹서버 부분
void setupMAINPAGE() {
server.on("/", [](){
String s = (const __FlashStringHelper *)MAIN_page; // 웹페이지 HTML 불러오기
strTemp = (String)t; // 온습도값을 문자열로 변환
strHumi = (String)h;
strsetTemp = (String)setTemp;
s.replace("@@TEMP@@", strTemp); // 변환된 온습도 수치를 HTML placeholder와 교체
s.replace("@@HUMI@@", strHumi);
s.replace("@@SETTEMP@@", strsetTemp);
server.send(200, "text/html", s); // HTML 코드를 반환
});
server.on("/temperature", [](){
server.send(200, "text/plain", String(t).c_str());
});
server.on("/humidity", [](){
server.send(200, "text/plain", String(h).c_str());
});
server.on("/heater", [](){
if (digitalRead(HEATPIN)) { // HEATPIN 상태를 확인해서
server.send(200, "text/plain", "ON"); // HIGH 상태면 "ON"
}
else {
server.send(200, "text/plain", "OFF"); // LOW 상태면 "OFF" 문자 전달
}
});
server.on("/settemp", [](){
if (server.arg("setTemp")=="set") { // textbox의 설정 온도값을 읽어와서
setTemp = server.arg("Temp").toFloat(); // 문자를 실수로 형변환
}
else if (server.arg("setTemp")=="tuneUP") {
setTemp = server.arg("Temp").toFloat() + 0.1; // + 버튼을 눌렀다면 설정온도 0.1도 증가
}
else {
setTemp = server.arg("Temp").toFloat() - 0.1; // - 버튼을 눌렀다면 설정온도 0.1도 감소
}
server.sendHeader("Location", "/"); // This Line Keeps It on Same Page
server.send(302, "text/plain", "Updated-- Press Back Button");
});
server.onNotFound([]() {
server.send(404,"text/plain", "404: Not found");
});
server.begin();
}
void setupMDNS() {
if (MDNS.begin("myesp")) { // Start the mDNS responder for esp8266.local
Serial.println("mDNS responder started");
} else {
Serial.println("Error setting up MDNS responder!");
}
}
// 설정된 interval 간격마다 온습도 확인
bool getData() {
unsigned long curMills = millis();
if (curMills - preMillsForDHT >= 5000)
{
preMillsForDHT = curMills;
h = dht.readHumidity() - 30; // 상대습도 읽기
t = dht.readTemperature(); // 섭씨온도 읽기
// Check if any reads failed and exit early (to try again).
if (isnan(h) || isnan(t)) {
Serial.println(F("Failed to read from DHT sensor!"));
return false;
}
Serial.print((String)t);
}
return true;
}
// 설정온도를 입력하면 현재온도와 비교해서
// 높으면 "true" 낮으면 "false" 을 반환하는 함수
void isNeedHeat(float setT) {
unsigned long curMills = millis();
if (curMills - preMillsForHeat >= 30000) {
preMillsForHeat = curMills;
if (setT > t) {
Serial.println("Heater ON!");
digitalWrite(HEATPIN, HIGH);
}
else
{
Serial.println("Heater OFF!");
digitalWrite(HEATPIN, LOW);
}
}
}
void setupOTA() {
// Port defaults to 8266
ArduinoOTA.setPort(8266);
// Hostname defaults to esp8266-[ChipID]
// ArduinoOTA.setHostname("myesp8266");
// No authentication by default
ArduinoOTA.setPassword("admin");
// Password can be set with it's md5 value as well
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
ArduinoOTA.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH) {
type = "sketch";
} else { // U_FS
type = "filesystem";
}
// NOTE: if updating FS this would be the place to unmount FS using FS.end()
Serial.println("Start updating " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) {
Serial.println("Auth Failed");
} else if (error == OTA_BEGIN_ERROR) {
Serial.println("Begin Failed");
} else if (error == OTA_CONNECT_ERROR) {
Serial.println("Connect Failed");
} else if (error == OTA_RECEIVE_ERROR) {
Serial.println("Receive Failed");
} else if (error == OTA_END_ERROR) {
Serial.println("End Failed");
}
});
ArduinoOTA.begin();
Serial.println("OTA Ready");
}
mainPage.html 파일
const char MAIN_page[] PROGMEM = R"=====(
<!DOCTYPE html>
<html lang="ko">
<head>
<meta name="viewport"content="width=device-width,initial-scale=1,user-scalable=no"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<style>
body{text-align:center;font-family:verdana;}
h2{font-size: 2.0rem}
button{border:0;border-radius:1.5rem;background-color:#0066ff;color:rgb(255, 255, 255);line-height:2.4rem;font-size:1.2rem;width:100%}
p { font-size: 1.6rem; }
.units { font-size: 1.2rem; }
.heading{
font-size: 1.5rem;
vertical-align:middle;
padding-bottom: 5px; padding-top: 15px;
}
</style>
</head>
<TITLE>
WIFI Controller
</TITLE>
<BODY>
<div style="text-align:center;display:inline-block;min-width:280px;">
<h2>온습도 모니터링</h2>
<p>
<span class="heading">온도/습도</span>
</p>
<p>
<span>
<i class="fas fa-thermometer-half" style="color:#059e8a;"></i>
<span id="temperature">@@TEMP@@</span>
<span class="units">°C / </span>
</span>
<span>
<i class="fas fa-tint" style="color:#00add6;"></i>
<span id="humidity">@@HUMI@@</span>
<span class="units">%</span>
</span>
</p>
<p><span class="heading">설정온도</span>
<form method="post" action="/settemp">
<input type="number" name="Temp" min="20" max="28" step="0.1" value="@@SETTEMP@@" style="min-width:100px; height:40px; font-size: 1.5rem;" >ºC
<button TYPE="SUBMIT" name="setTemp" VALUE="set" style="width:70px; background-color:#2b499b;">set</button>
<br>
<button TYPE="SUBMIT" name="setTemp" VALUE="tuneUP" style="width:40%; background-color:#e91f1f;" >+</button>
<button TYPE="SUBMIT" name="setTemp" VALUE="tuneDW" style="width:40%;">-</button>
</form>
</p>
<p>온풍기 상태: <b id="heater">확인중...</b> </p>
</div>
</BODY>
<script>
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("temperature").innerHTML = this.responseText;
}
};
xhttp.open("GET", "/temperature", true);
xhttp.send();
}, 10000 ) ;
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("humidity").innerHTML = this.responseText;
}
};
xhttp.open("GET", "/humidity", true);
xhttp.send();
}, 10000 ) ;
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("heater").innerHTML = this.responseText;
}
};
xhttp.open("GET", "/heater", true);
xhttp.send();
}, 5000 ) ;
</script>
</HTML>
)=====";
platformio.ini 파일
[common]
lib_deps =
ESP8266 @ ^1.0
adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/DHT sensor library @ ^1.1.4
[env:esp8266]
platform = espressif8266
board = esp12e
framework = arduino
lib_deps = ${common.lib_deps}
monitor_speed = 115200
board_build.flash_mode = dio
upload_resetmethod = nodemcu
upload_speed = 115200
[env:esp8266OTA]
platform = espressif8266
board = esp12e
framework = arduino
monitor_speed = 115200
board_build.flash_mode = dio
lib_deps =
ESP8266 @ ^1.0
adafruit/Adafruit Unified Sensor @ ^1.1.4
adafruit/DHT sensor library @ ^1.1.4
upload_resetmethod = nodemcu
upload_protocol = espota
upload_port = 10.11.85.70
upload_flags =
--port=8266
--auth=admin
결과
펌웨어를 업로드하고 서버에 접속하면 다음과 같은 화면을 만날 수 있습니다. 음.. 이나마도 어디서 베껴온 건데 역시 전 미적 감각이...
설정온도와 현제 온도를 비교해서 온도가 낮으면 릴레이를 켜고 릴레이의 상태가 ON이면 화면의 온풍기 상태도 ON으로 표시됩니다.
아직 실전 테스트를 해보지 않아서 약간 걱정이 되긴 하는데, 이제 캠핑장에서도 온풍기 켜놓고 좀 편안하게 잠을 잘 수 있을지 기대가 됩니다.
아! 220V 전기는 장난감이 아닙니다. 정확한 이해가 없이는 위험할 수 있으니 무작정 따라 하는 건 자재해 주세요.
PS. 지난 주말에 캠핑장 예약에 성공해서 오랜만에 캠핑 다녀왔습니다. 아주 추운 날씨는 아니었지만 새벽엔 쌀쌀해서 온풍기 사용했는데요. 25도 정도 설정해 두니 아주 포근하게 잘 잘 수 있었습니다. 캠핑장에서는 스마트폰의 핫스폿을 켜서 스마트 콘센트와 연결을 했습니다. 사전에 펌웨어 컴파일할 때 요건 미리 준비해야겠죠.
온풍기 상태 확인하는데 몇초가량 딜레이가 있는 점 그리고 온풍기를 바로 켜고 끌 수 있는 인터페이스가 필요한 점 등 아쉬운 점이 있었지만 아주 잘 작동해 주었답니다. 만족입니다.
끝!
'Hardware > MCU(Arduino,ESP8266)' 카테고리의 다른 글
벽돌이 된 wemos d1 mini 소생시키기 (0) | 2021.12.12 |
---|---|
온풍기용 스마트 플러그 업그레이드 - WebSocket으로 동적 제어 및 센서값 기록하기 (0) | 2021.09.18 |
ESP8266, Watchdog (wdt reset error) (4) | 2020.12.09 |
배터리를 사용하는 아두이노 프로젝트에 배터리 잔량 표시하기 (4) | 2020.12.07 |
댓글