내 식물들이 항상 물 과다 또는 부족으로 고통받고, 요리에 허브를 많이 사용하는 편이라 맞춤형 관수 시스템을 만들기로 했습니다. 허브 박스는 설정 가능하고 자동/수동 모드 모두 지원해야 합니다. 따라서 웹사이트 인터페이스를 통해 설정을 관리하고 습도를 차트로 확인할 수 있게 했습니다. 마지막 단계로 음성 제어를 통합해 아마존 알렉사에게 습도 확인, 식물 생장 램프 켜기/끄기, 자동 모드 비활성화 시 관수 시작을 요청할 수 있게 했습니다. 결과물 확인하기.
기술적 부분부터 시작해 아두이노를 구입했습니다. 여러 튜토리얼을 거치며 소프트웨어와 아두이노 제어에 익숙해졌습니다. 와이파이 컨트롤러, 습도 센서, 펌프, 식물 생장 램프, 추가 하드웨어(램프와 펌프 회로를 아두이노와 분리하는 릴레이 쉴드, 전선, 너도밤나무 프레임용 목재)를 주문했습니다. 이 튜토리얼에는 결과물의 아두이노 코드와 부품 활용법이 포함되어 있습니다. 웹사이트/API 코드는 수요가 많지 않으면 제공하지 않습니다(요청이 많으면 고려하겠습니다 ;)).
1단계: 습도 센서
첫 번째 목표는 아두이노로 습도를 읽는 것이었습니다. YL-69 습도 센서는 아두이노와 쉽게 연결됩니다. VCC 핀을 GPIO 핀(예시: 핀 6)에, 그라운드를 그라운드에, A0을 아날로그 핀(예시: A1)에 연결하면 됩니다.
에스프레시프 ESP8266 ESP-01을 아두이노에 연결해 와이파이를 구현하는 게 가장 어려웠습니다. 스크립트에서 와이파이 동작까지 몇 시간이 걸렸습니다.
ESP 연결: VCC = 3.3V, GND = GND, CH_PD = 3.3V, TX = 핀 2, RX = 핀 3. 실제 사용 시 핀 2, 3에 5V→3.3V 레벨 컨버터 사용 권장. 내 경우엔 없이도 잘 동작했습니다.
아두이노와 마찬가지로 ESP-01도 별도 마이크로컨트롤러입니다. 두 컨트롤러 간 통신하려면 시리얼 통신 필요. 아두이노 우노는 기본적으로 핀 0, 1을 RX/TX로 쓰지만 USB 디버깅용이기도 하므로 SoftwareSerial.h를 포함해 커스텀 핀을 정의하는 게 좋습니다.
#include <SoftwareSerial.h>
SoftwareSerial espSerial(3, 2); // RX, TX
void setup() {
Serial.begin(9600);
espSerial.begin(115200); // AT+UART_DEF=9600,8,1,0,0 후 9600으로 변경
while (!Serial);
}
void loop() {
if (espSerial.available()) {
Serial.write(espSerial.read());
}
if (Serial.available()) {
espSerial.write(Serial.read());
}
}
위 스크립트 실행 후 시리얼 모니터에서 AT 명령어를 입력하고 결과를 볼 수 있습니다. 시리얼 통신은 실패하기 쉬우므로 ESP 통신 보드레이트를 115200에서 9600으로 낮췄습니다.
스크립트는 HTTP 1.0을 사용합니다. HTTP 1.1은 응답에 바이트가 포함되기 때문입니다. AT+CIPSEND 후 명령 전송을 위한 줄바꿈 처리가 중요합니다. 잘못되면 바이트 전송 에러 발생.
#include <SoftwareSerial.h>
SoftwareSerial espSerial(3, 2); // RX, TX
const char* ssid = "<YOUR-WIFI-SSID>";
const char* pass = "<YOUR-WIFI-PASSWORD>";
void setup() {
Serial.begin(9600);
espSerial.begin(9600);
while(!Serial);
while(!connectToWiFi());
// 웹사이트 요청 및 결과 출력
if (httpRequest("my.server.com", "/site/subsite/index.php")) {
while (espSerial.available()) {
Serial.write(espSerial.read());
}
}
}
void loop() {
if (espSerial.available()) {
Serial.write(espSerial.read());
}
if (Serial.available()) {
espSerial.write(Serial.read());
}
}
bool connectToWiFi() {
delay(2000);
espSerial.setTimeout(3000);
while (espSerial.available()) Serial.write(espSerial.read());
Serial.println(F("[ESP] 와이파이 연결 중"));
espSerial.println(F("AT+CIPSTATUS=2"));
if (!espSerial.find("OK")) {
espSerial.setTimeout(10000);
Serial.println(F("[ESP] 모듈 리셋"));
espSerial.println(F("AT+RST"));
if (!espSerial.find("ready")) {
Serial.println(F("[ESP] 리셋 실패"));
return false;
}
Serial.println(F("[ESP] CWMode 설정"));
espSerial.println(F("AT+CWMODE=1"));
if (!espSerial.find("OK")) {
Serial.println(F("[ESP] 모드 실패"));
return false;
}
Serial.println(F("[ESP] 라우터 연결"));
espSerial.print(F("AT+CWJAP=\""));
espSerial.print(ssid);
espSerial.print(F("\",\""));
espSerial.print(pass);
espSerial.println("\"\"");
if (!espSerial.find("OK")) {
Serial.println(F("[ESP] 와이파이 연결 실패"));
return false;
}
}
espSerial.setTimeout(3000);
Serial.println(F("[ESP] 와이파이 연결됨"));
return true;
}
bool httpRequest(String server, String site) {
String cmd = "";
cmd += "GET " + site + " HTTP/1.0\r\n";
cmd += "Host: " + server + "\r\n";
cmd += "Connection: close";
int cmdLength = cmd.length() + 4;
espSerial.print(F("AT+CIPSTART=\"TCP\",\""));
espSerial.print(server);
espSerial.println(F("\",80"));
if (!espSerial.find("OK")) {
Serial.println(F("[ESP] TCP 연결 에러"));
return false;
}
espSerial.print(F("AT+CIPSEND="));
espSerial.println(cmdLength);
if (!espSerial.find(">")) {
Serial.println(F("[ESP] 전송 상태 에러"));
return false;
}
espSerial.print(F("GET "));
espSerial.print(site);
espSerial.print(F(" HTTP/1.0\r\n"));
espSerial.print(F("Host: "));
espSerial.print(server);
espSerial.print(F("\r\n"));
espSerial.print(F("Connection: close\r\n"));
espSerial.println();
if (!espSerial.find(":")) {
Serial.println(F("바이트 미전송"));
espSerial.print(F("AT+CIPCLOSE"));
return false;
}
char status[32] = {0};
espSerial.readBytesUntil('\r', status, sizeof(status));
if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
Serial.print(F("[ESP] 예기치 않은 응답: "));
Serial.println(status);
return false;
}
if (!espSerial.find("\r\n\r\n")) {
Serial.println(F("[ESP] 유효하지 않은 응답"));
return false;
}
return true;
}
4단계: 나무 상자 제작
프레임은 모든 전자 부품과 슈퍼마켓 허브 화분 3개를 수납하도록 설계했습니다. 모든 부품 크기를 측정하고 위치를 배치했습니다. 습도 센서 4개, 펌프 2개, 아두이노+쉴드, 4채널 릴레이 쉴드, USB 플러그, 전선 등이 들어가야 합니다. 너도밤나무로 제작해 견고하고 추가 도장 없이 물방울에도 견디게 했습니다.
원형 구멍은 지그쏘로 직접 만든 지그쏘 테이블에서 뚫었습니다. 화분 마운트는 핫글루로 원형 안쪽에 고정. 상자 측면은 목재 접착제(D3 방수 등급)로 접착. 전자 부품 외에는 하단 패널 고정용 외에 나사나 못을 쓰지 않았습니다.
모든 회로, 전선, 물 튜브를 상자 안에 넣고 센서는 밖으로, 물 탱크용 튜브는 밖으로 뺐습니다. 상자 닫기 전에 전자 부품 보호를 위해 받침 접시를 추가해 물이 안으로 들어가지 않게 했습니다.
5단계: 웹사이트 API
API와 웹사이트는 jQuery, Bootstrap, X-editable(인라인 AJAX 폼), Chart.js(습도 차트) 기반으로 PHP로 구현했습니다. 웹사이트에서 아두이노 설정(센서 핀, 습도 체크 주기, 식물별 펌프, 펌프 VCC 핀, 조명 VCC 핀)을 정의하고 현재 습도와 차트를 확인할 수 있습니다.