산업 제조
산업용 사물 인터넷 | 산업자재 | 장비 유지 보수 및 수리 | 산업 프로그래밍 |
home  MfgRobots >> 산업 제조 >  >> Manufacturing Technology >> 제조공정

스마트 허브 박스 에코 시스템: 알렉사 연동 실내 가드닝 키트

부품 및 소모품

부품 수량
아마존 알렉사 에코 닷 (Echo Dot) 1
아두이노 우노 (Arduino UNO) 1
에스프레시프 ESP8266 ESP-01 1
아두이노 프로토 쉴드 1
아두이노 4채널 릴레이 쉴드 1
아쿠아 펌프 (수중 펌프) 2
식물 생장 램프 1
220V DC 파워 어댑터 1
12V DC 파워 어댑터 1
USB 전원 플러그 + 케이블 1
에어 호스 1
점퍼 와이어 (범용) 1세트

앱 및 온라인 서비스

서비스
아두이노 IDE
아마존 알렉사 스킬 키트 (Alexa Skills Kit)
아마존 웹 서비스 AWS Lambda
PHP 웹 서버

프로젝트 개요

내 식물들이 항상 물 과다 또는 부족으로 고통받고, 요리에 허브를 많이 사용하는 편이라 맞춤형 관수 시스템을 만들기로 했습니다. 허브 박스는 설정 가능하고 자동/수동 모드 모두 지원해야 합니다. 따라서 웹사이트 인터페이스를 통해 설정을 관리하고 습도를 차트로 확인할 수 있게 했습니다. 마지막 단계로 음성 제어를 통합해 아마존 알렉사에게 습도 확인, 식물 생장 램프 켜기/끄기, 자동 모드 비활성화 시 관수 시작을 요청할 수 있게 했습니다. 결과물 확인하기.

기술적 부분부터 시작해 아두이노를 구입했습니다. 여러 튜토리얼을 거치며 소프트웨어와 아두이노 제어에 익숙해졌습니다. 와이파이 컨트롤러, 습도 센서, 펌프, 식물 생장 램프, 추가 하드웨어(램프와 펌프 회로를 아두이노와 분리하는 릴레이 쉴드, 전선, 너도밤나무 프레임용 목재)를 주문했습니다. 이 튜토리얼에는 결과물의 아두이노 코드와 부품 활용법이 포함되어 있습니다. 웹사이트/API 코드는 수요가 많지 않으면 제공하지 않습니다(요청이 많으면 고려하겠습니다 ;)).

1단계: 습도 센서

첫 번째 목표는 아두이노로 습도를 읽는 것이었습니다. YL-69 습도 센서는 아두이노와 쉽게 연결됩니다. VCC 핀을 GPIO 핀(예시: 핀 6)에, 그라운드를 그라운드에, A0을 아날로그 핀(예시: A1)에 연결하면 됩니다.

튜토리얼: 토양 습도 센서

byte vccPin = 6; 
byte dataPin = A1; 

void setup() { 
  pinMode(vccPin, OUTPUT); 
  digitalWrite(vccPin, LOW); 
  Serial.begin(9600); 
  while (!Serial); 
} 

int readHumidity() { 
  digitalWrite(vccPin, HIGH); 
  delay(500); // 측정 전 프리파워 시간 테스트 필요 
  int value = analogRead(dataPin); 
  digitalWrite(vccPin, LOW); 
  return 1023 - value; 
} 

void loop() { 
  Serial.print("습도 레벨 (0-1023): "); 
  Serial.println(readHumidity()); 
  delay(10000); 
}

2단계: 펌프와 램프용 릴레이

다음 목표는 릴레이 쉴드(4채널)로 램프, 펌프, 아두이노 회로를 분리하는 것입니다. 아두이노는 5V, 펌프는 12V, 식물 생장 램프는 230V를 사용합니다. 쉴드는 아두이노 5V와 그라운드 핀에 연결합니다. 각 릴레이는 원하는 GPIO 핀으로 제어합니다. 마지막으로 쉴드의 VCC JC를 VCC에 점퍼로 연결하거나 별도 배터리 사용(최선이지만 본 프로젝트엔 미포함).

중요: 이 쉴드는 핀이 LOW일 때 "On" 상태가 됩니다. 핀을 OUTPUT으로 정의하는 순간 자동으로 활성화됩니다. 코드에서 릴레이를 끄려면 항상 INPUT과 LOW로 설정해야 합니다. 기본적으로 아두이노 핀은 INPUT과 LOW 상태입니다.

튜토리얼: 릴레이 쉴드 | 왜 OUTPUT + LOW = Active인가?

byte pump1 = 11; 
byte pump2 = 10; 

void setup() { 
  Serial.begin(9600); 
  while (!Serial); 
  pinMode(pump1, OUTPUT); // LOW/HIGH 변형 
  digitalWrite(pump2, LOW); // INPUT/OUTPUT 변형 
} 

void loop() { 
  digitalWrite(pump1, HIGH); // pump1 비활성화 
  pinMode(pump2, INPUT); // pump2 비활성화 
  delay(1000); 
  digitalWrite(pump1, LOW); // pump1 활성화 
  pinMode(pump2, OUTPUT); // pump2 활성화 
  delay(1000); 
}

3단계: ESP-01로 와이파이 연결

에스프레시프 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으로 낮췄습니다.

튜토리얼: ESP8266 + 아두이노 | 일반 ESP8266 튜토리얼 (독일어)

스크립트는 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 핀)을 정의하고 현재 습도와 차트를 확인할 수 있습니다.

스마트 허브 박스 에코 시스템: 알렉사 연동 실내 가드닝 키트

설정은 JSON 형태로 아두이노에 제공됩니다. 시작 후 일정 주기로 허브 박스가 새 설정을 확인합니다. 아두이노에서 JSON 파싱용으로 ArduinoJson 라이브러리, 폴링 주기용으로 StensTimer 라이브러리를 사용했습니다.

6단계: 알렉사 연동

웹사이트는 알렉사 통신용 API를 제공합니다. 알렉사로부터 요청 JSON을 받아 아두이노용 커스텀 JSON(예: 램프 켜기, 식물 1 관수 등)으로 변환하는 허브 역할을 합니다. 아두이노가 새 액션을 폴링해 실행합니다.

스마트 허브 박스 에코 시스템: 알렉사 연동 실내 가드닝 키트

음성 요청이 단순 온오프 이상이므로 알렉사 스마트 홈이 아닌 알렉사 스킬을 구현했습니다. AWS Lambda가 요청 JSON을 내 API로 전달하고, API가 인텐트를 파싱합니다.

var https = require('https'); 
exports.handler = (event, context, callback) => { 
  var postData = JSON.stringify(event); 
  var options = { 
    host: '<MY-SERVER>', 
    path: '<MY-API-SITE>', 
    port: 443, 
    method: 'POST', 
    headers: { 
      'Content-Type': 'application/json', 
      'Content-Length': postData.length, 
    } 
  }; 
  var postRequest = https.request(options, function(res) { 
    res.setEncoding('utf8'); 
    res.on('data', function (chunk) { 
      console.log('Response: ' + chunk); 
      callback(null, JSON.parse(chunk)); 
    }); 
  }); 
  postRequest.write(postData); 
  postRequest.end(); 
};

사용한 인텐트 예시:

독일어와 영어 로케일 지원도 추가했습니다.

결과물

슈퍼마켓 허브 화분을 넣고, 물 튜브와 습도 센서를 흙에, 튜브를 외부 물 탱크에 연결한 나무 상자가 완성되었습니다. 알렉사 연동으로 다음 명령들이 가능합니다:

스마트 허브 박스 에코 시스템: 알렉사 연동 실내 가드닝 키트

알렉사에게 식물 습도 묻고 관수하기 (독일어):

알렉사에게 식물 생장 램프 켜기:

영상 없는 GIF 결과:

스마트 허브 박스 에코 시스템: 알렉사 연동 실내 가드닝 키트

스마트 허브 박스 에코 시스템: 알렉사 연동 실내 가드닝 키트

향후 계획 기능

업데이트 (2018.03.23)

새 인텐트 2개 추가. 외부 아두이노 나노가 습도만 기록하는 향후 기능에 중요합니다.

코드

EcoActionBuffer.h

#ifndef ECOACTIONBUFFER_H 
#define ECOACTIONBUFFER_H 
#include "Arduino.h" 
#include "StensTimer.h" 

struct EcoActionBuffer : public IStensTimerListener { 
  long entryNo; 
  int action; 
  int pin; 
  long duration; 
  void timerCallback(Timer* timer); 
  void switchPin(int pin, bool value); 
  void readStack(); 
  void process(); 
  void toSerial(); 
  void reset(); 
}; 
#endif

EcoActionBuffer.cpp

#include "EcoActionBuffer.h" 
#include "StensTimer.h" 
#include "WhiteWalnutApi.h" 

#define ACTION_ECOACTION_READ 1 
#define ACTION_ECOACTION_HIGH 2 
#define ACTION_ECOACTION_LOW 3 

void EcoActionBuffer::readStack() { 
  reset(); 
  WhiteWalnutApi::receiveActionFromStack(*this); 
  if (entryNo != 0) { 
    process(); 
    // WhiteWalnutApi::updateActionOnStack(*this); // 성능상 비활성화 
  } 
} 

void EcoActionBuffer::process() { 
  toSerial(); 
  pinMode(pin, OUTPUT); 
  digitalWrite(pin, HIGH); 
  switch (action) { 
    case ACTION_ECOACTION_HIGH: 
      switchPin(pin, true); 
      break; 
    case ACTION_ECOACTION_LOW: 
      switchPin(pin, false); 
      break; 
  } 
  if (duration != 0) { 
    StensTimer::getInstance()->setTimer(this, -pin, duration); 
  } 
} 

void EcoActionBuffer::timerCallback(Timer* timer) { 
  switch (timer->getAction()) { 
    case ACTION_ECOACTION_READ: 
      readStack(); 
      break; 
  } 
  if (timer->getAction() < 0) { 
    switchPin(abs(timer->getAction()), false); 
  } 
} 

void EcoActionBuffer::switchPin(int pin, bool value) { 
  switch (value) { 
    case true: 
      digitalWrite(pin, LOW); 
      break; 
    case false: 
      digitalWrite(pin, HIGH); 
      break; 
  } 
  WhiteWalnutApi::switchPin(pin, value); 
} 

void EcoActionBuffer::reset() { 
  entryNo = 0; 
  action = 0; 
  pin = 0; 
  duration = 0; 
} 

void EcoActionBuffer::toSerial() { 
  Serial.print(entryNo); 
  Serial.print(F(" - Action: ")); 
  Serial.print(action); 
  Serial.print(F(", Pin: ")); 
  Serial.print(pin); 
  Serial.print(F(", Duration: ")); 
  Serial.print(duration); 
  Serial.println(); 
}

Plant.cpp

#include "Plant.h" 
#include "StensTimer.h" 
#include "WhiteWalnutApi.h" 

#define ACTION_PLANT_CHECKHUMIDITY 2 
#define PIN_HUMIDITY_VCC 12 

void Plant::checkHumidity() { 
  if (humidityDataPin != 0) { 
    Serial.print(code); 
    Serial.print(F(" - 습도 체크...")); 
    digitalWrite(PIN_HUMIDITY_VCC, HIGH); 
    delay(200); 
    int humidity = 1023 - analogRead(humidityDataPin); 
    digitalWrite(PIN_HUMIDITY_VCC, LOW); 
    Serial.println(humidity); 
    WhiteWalnutApi::sendHumidity(*this, humidity); 
    if (humidityCheckInterval == 0) humidityCheckInterval = 60000; 
    StensTimer::getInstance()->setTimer(this, ACTION_PLANT_CHECKHUMIDITY, humidityCheckInterval); 
  } else 
    StensTimer::getInstance()->setTimer(this, ACTION_PLANT_CHECKHUMIDITY, 60000); 
} 

void Plant::updateApi() { 
  WhiteWalnutApi::updatePlant(*this); 
  // WhiteWalnutApi::sendHeartbeat(*this); // 성능상 비활성화 
  pinMode(PIN_HUMIDITY_VCC, OUTPUT); 
  toSerial(); 
} 

void Plant::timerCallback(Timer* timer) { 
  switch (timer->getAction()) { 
    case ACTION_PLANT_CHECKHUMIDITY: 
      checkHumidity(); 
      break; 
  } 
} 

void Plant::toSerial() { 
  Serial.print(code); 
  Serial.print(F(" - DataPin: ")); 
  Serial.print(humidityDataPin); 
  Serial.print(F(", Interval: ")); 
  Serial.print(humidityCheckInterval); 
  Serial.println(); 
}

Plant.h

#ifndef PLANT_H 
#define PLANT_H 
#include "Arduino.h" 
#include "StensTimer.h" 

struct Plant : public IStensTimerListener { 
  const char* code; 
  int humidityDataPin; 
  long humidityCheckInterval; 
  void checkHumidity(); 
  void timerCallback(Timer* timer); 
  void toSerial(); 
  void updateApi(); 
}; 
#endif

WhiteWalnut.ino (메인 스케치)

#include "EcoActionBuffer.h" 
#include "Plant.h" 
#include "StensTimer.h" 
#include "WhiteWalnutApi.h" 

struct TimerHelper : public IStensTimerListener { 
  public: 
    void updateApi(); 
    void timerCallback(Timer* timer); 
}; 

StensTimer* stensTimer; 
TimerHelper apiTimer; 
Plant leftPlant; 
Plant centerPlant; 
Plant rightPlant; 
Plant externalPlant; 
EcoActionBuffer actionBuffer; 

#define ACTION_PLANT_UPDATE 1 
#define ACTION_ECOACTION_READ 1 

void setup() { 
  Serial.begin(9600); 
  while (!Serial); 
  stensTimer = StensTimer::getInstance(); 
  leftPlant.code = "LEFT"; 
  centerPlant.code = "CENTER"; 
  rightPlant.code = "RIGHT"; 
  externalPlant.code = "EXTERNAL"; 
  while(!WhiteWalnutApi::connectToWiFi()) delay(2000); 
  WhiteWalnutApi::switchPin(0, false); 
  apiTimer.updateApi(); 
  leftPlant.checkHumidity(); 
  centerPlant.checkHumidity(); 
  rightPlant.checkHumidity(); 
  externalPlant.checkHumidity(); 
  actionBuffer.readStack(); 
  StensTimer::getInstance()->setInterval(&apiTimer, ACTION_PLANT_UPDATE, 60000); 
  StensTimer::getInstance()->setInterval(&actionBuffer, ACTION_ECOACTION_READ, 1000); 
} 

void loop() { 
  stensTimer->run(); 
} 

void TimerHelper::updateApi() { 
  leftPlant.updateApi(); 
  centerPlant.updateApi(); 
  rightPlant.updateApi(); 
  externalPlant.updateApi(); 
} 

void TimerHelper::timerCallback(Timer* timer){ 
  switch (timer->getAction()) { 
    case ACTION_PLANT_UPDATE: 
      updateApi(); 
      break; 
  } 
}

WhiteWalnutApi.cpp

// 와이파이 및 API 설정 필요 
#include "Arduino.h" 
#include "ArduinoJson.h" 
#include "EcoActionBuffer.h" 
#include "MemoryFree.h" 
#include "Plant.h" 
#include "SoftwareSerial.h" 
#include "WhiteWalnutApi.h" 

SoftwareSerial espSerial(3, 2); 
const char* ssid = "<MY-SSID>"; 
const char* pass = "<MY-PASSWORD>"; 
const char* API_SERVER = "<MY-SERVER>"; 
const char* API_PLANT = "<MY-PLANT-API>"; 
const char* API_ACTION = "<MY-ACTION-API>"; 
char* findOK = "OK"; 
char* findRY = "ready"; 
char* findGT = ">"; 
char* findDP = ":"; 
char* findHD = "\r\n\r\n"; 
char* findBT = "\r\n"; 

bool WhiteWalnutApi::connectToWiFi() { 
  espSerial.begin(9600); 
  espSerial.setTimeout(3000); 
  while (espSerial.available()) Serial.write(espSerial.read()); 
  Serial.println(F("[ESP] 와이파이 연결 중")); 
  espSerial.println(F("AT+CIPSTATUS=2")); 
  if (!espSerial.find(findOK)) { 
    espSerial.setTimeout(10000); 
    Serial.println(F("[ESP] 모듈 리셋")); 
    espSerial.println(F("AT+RST")); 
    if (!espSerial.find(findRY)) { 
      Serial.println(F("[ESP] 리셋 실패")); 
      return false; 
    } 
    Serial.println(F("[ESP] CWMode 설정")); 
    espSerial.println(F("AT+CWMODE=1")); 
    if (!espSerial.find(findOK)) { 
      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(findOK)) { 
      Serial.println(F("[ESP] 와이파이 연결 실패")); 
      return false; 
    } 
  } 
  espSerial.setTimeout(3000); 
  Serial.println(F("[ESP] 와이파이 연결됨")); 
  return true; 
} 

void WhiteWalnutApi::updatePlant(Plant& plant) { 
  String site = String(API_PLANT) + "?action=get&code=" + String(plant.code); 
  while (!httpRequest(site)) connectToWiFi(); 
  JsonObject& root = parseJson(); 
  if (root.success()) { 
    plant.humidityDataPin = root["dataPin"].as<int>(); 
    plant.humidityCheckInterval = atol(root["interval"].as<char*>()); 
  } 
} 

void WhiteWalnutApi::sendHumidity(Plant& plant, int humidity) { 
  String site = String(API_PLANT) + "?action=humidity&code=" + String(plant.code) + "&humidity=" + String(humidity); 
  while (!httpRequest(site)) connectToWiFi(); 
} 

void WhiteWalnutApi::sendHeartbeat(Plant& plant) { 
  String site = String(API_PLANT) + "?action=heartbeat&code=" + String(plant.code); 
  while (!httpRequest(site)) connectToWiFi(); 
} 

void WhiteWalnutApi::receiveActionFromStack(EcoActionBuffer& actionBuffer) { 
  while (!httpRequest(String(API_ACTION))) connectToWiFi(); 
  JsonObject& root = parseJson(); 
  if (root.success()) { 
    actionBuffer.entryNo = atol(root["entryNo"].as<char*>()); 
    actionBuffer.action = root["actionEnum"].as<int>(); 
    actionBuffer.pin = root["pin"].as<int>(); 
    actionBuffer.duration = atol(root["value"].as<char*>()); 
  } 
} 

void WhiteWalnutApi::updateActionOnStack(EcoActionBuffer& actionBuffer) { 
  String site = String(API_ACTION) + "?action=processed&entryNo=" + String(actionBuffer.entryNo); 
  while (!httpRequest(site)) connectToWiFi(); 
} 

void WhiteWalnutApi::switchPin(int pin, bool value) { 
  String site = String(API_ACTION) + "?action=switch&pin=" + String(pin) + "&value=" + String(value); 
  while (!httpRequest(site)) connectToWiFi(); 
} 

bool WhiteWalnutApi::httpRequest(String site) { 
  int cmdLength = 44 + site.length() + strlen(API_SERVER); 
  espSerial.print(F("AT+CIPSTART=\"TCP\",\"")); 
  espSerial.print(API_SERVER); 
  espSerial.println(F("\",80")); 
  if (!espSerial.find(findOK)) { 
    Serial.println(F("[ESP] TCP 연결 에러")); 
    return false; 
  } 
  espSerial.print(F("AT+CIPSEND=")); 
  espSerial.println(cmdLength); 
  if (!espSerial.find(findGT)) { 
    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(API_SERVER); 
  espSerial.print(F("\r\n")); 
  espSerial.print(F("Connection: close\r\n")); 
  espSerial.println(); 
  if (!espSerial.find(findDP)) { 
    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(findHD)) { 
    Serial.println(F("[ESP] 유효하지 않은 응답")); 
    return false; 
  } 
  return true; 
} 

JsonObject& WhiteWalnutApi::parseJson() { 
  const size_t capacity = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2) + 60; 
  DynamicJsonBuffer jsonBuffer(capacity); 
  JsonObject& root = jsonBuffer.parseObject(espSerial); 
  if (!root.success()) Serial.println(F("파싱 실패!")); 
  return root; 
}

WhiteWalnutApi.h

#ifndef WHITEWALNUTAPI_H 
#define WHITEWALNUTAPI_H 
#include "Arduino.h" 
#include "ArduinoJson.h" 
#include "EcoActionBuffer.h" 
#include "Plant.h" 

class WhiteWalnutApi { 
public: 
  static bool connectToWiFi(); 
  static void updatePlant(Plant& plant); 
  static void sendHumidity(Plant& plant, int humidity); 
  static void sendHeartbeat(Plant& plant); 
  static void receiveActionFromStack(EcoActionBuffer& actionBuffer); 
  static void updateActionOnStack(EcoActionBuffer& actionBuffer); 
  static void switchPin(int pin, bool value); 
private: 
  static bool httpRequest(String site); 
  static JsonObject& parseJson(); 
}; 
#endif

회로도

통신 및 인터페이스 구조도

스마트 허브 박스 에코 시스템: 알렉사 연동 실내 가드닝 키트

알렉사에게 물어볼 수 있는 모든 인텐트와 응답 (다국어)

스마트 허브 박스 에코 시스템: 알렉사 연동 실내 가드닝 키트

알렉사 JSON 파싱 진입 단계

스마트 허브 박스 에코 시스템: 알렉사 연동 실내 가드닝 키트

제조공정

  1. 웹 제어 DMX 조명 시스템 - 아두이노 마스터 컨트롤러
  2. 블루투스 제어 아두이노 스파이봇(Spybot) 만들기
  3. SERENA: TFT LCD 터치스크린 탑재 아두이노 메가 2560 커스텀 알람 시스템
  4. 컴팩트 기상 관측소: 아두이노 나노 & ESP8266, BMP280 기압계 & OLED/노키아 LCD
  5. 스마트 지하실 환기 시스템: 아두이노 기반 습도 제어
  6. 아두이노와 구글 시트로 구현하는 실시간 RFID 출석 시스템
  7. HC-05 블루투스 모듈을 활용한 무선 아두이노 프로그래밍 실드 – USB 케이블 불필요
  8. 아두이노 기반 충돌 경고 시스템 – 시각/청각 알림으로 차량 안전 강화
  9. 아두이노 나노용 고성능 2.8" TFT 실드 – 320×240 SPI 디스플레이
  10. 아두이노 우노/메가용 닉시관 클록 실드 NCS314 – IN-14/IN-12 호환