Walter Heger
Published © GPL3+

Herb Box Eco System

Log humidity, automatic irrigate, switch plant growing lamp on and voice control everything with Amazon Alexa and Arduino.

IntermediateFull instructions providedOver 1 day99,942
Herb Box Eco System

Things used in this project

Story

Read more

Schematics

Communication Flow

Chart about the communication and interfaces

Amazon Alexa Intents

All implemented intents you can ask Alexa, including the response.
(multilingual)

Some Source Code of PHP integration

If you are interested in the entrance step of your alexa json parsing.

Code

EcoActionBuffer.h

Arduino
#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

Arduino
#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); // deactivated for performance
  }
}

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

Arduino
#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(" - Check humidity..."));
    digitalWrite(PIN_HUMIDITY_VCC, HIGH);
    delay(200); // TODO
    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); // deactivated for performance
  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

Arduino
#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

Arduino
#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

Arduino
you need to add your WiFi and API settings
#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] Connecting to WiFi"));
  espSerial.println(F("AT+CIPSTATUS=2"));
  if (!espSerial.find(findOK)) {
    espSerial.setTimeout(10000);
    Serial.println(F("[ESP] Reset Module"));
    espSerial.println(F("AT+RST")); if (!espSerial.find(findRY)) { Serial.println(F("[ESP] Reset failed")); return false; }
    Serial.println(F("[ESP] Set CWMode"));
    espSerial.println(F("AT+CWMODE=1")); if (!espSerial.find(findOK)) { Serial.println(F("[ESP] Mode failed")); return false; }
    Serial.println(F("[ESP] Connect to Router"));
    espSerial.print(F("AT+CWJAP=\""));
    espSerial.print(ssid);
    espSerial.print(F("\",\""));
    espSerial.print(pass);
    espSerial.println("\"");
    if (!espSerial.find(findOK)) { Serial.println(F("[ESP] WiFi connection failed")); return false; }
  }
  espSerial.setTimeout(3000);
  Serial.println(F("[ESP] WiFi is connected"));
  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(); // TODO: REMOVE RETURN
}

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) {

  // char* cmd;
  // sprintf(cmd, "GET %s HTTP/1.0\r\nHost: %s\r\nConnection: close", site, API_SERVER);

  /*
  String cmd = "";
  cmd += "GET " + site + " HTTP/1.0\r\n";
  cmd += "Host: " + String(API_SERVER) + "\r\n";
  cmd += "Connection: close";
  int cmdLength = cmd.length() + 4;
  Serial.println(cmd);
  */
  int cmdLength = 44 + site.length() + strlen(API_SERVER);
  
  // Serial.print(F("[MEMORY] "));
  // Serial.print(freeMemory());
  // Serial.print(F(" - "));
  // Serial.println(site);
  // -> 785 for external
      
  espSerial.print(F("AT+CIPSTART=\"TCP\",\""));
  espSerial.print(API_SERVER);
  espSerial.println(F("\",80"));
  if (!espSerial.find(findOK)) { Serial.println(F("[ESP] TCP Connection Error")); return false; }

  espSerial.print(F("AT+CIPSEND="));
  espSerial.println(cmdLength);
  // espSerial.println(strlen(cmd));
  if (!espSerial.find(findGT)) { Serial.println(F("[ESP] Send State Error")); 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();
  // while (espSerial.available()) Serial.println(espSerial.readString()); return;
  if (!espSerial.find(findDP)) { Serial.println(F("Bytes not sent")); 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] Unexpected response: ")); Serial.println(status); return false; } // Check HTTP status

  if (!espSerial.find(findHD)) { Serial.println(F("[ESP] Invalid response")); return false; } // Skip HTTP headers
  // if (!espSerial.find(findBT)) { Serial.println(F("[ESP] Bytes not found")); return; } // skip bytes (for http 1.1)

  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("Parsing failed!"));
  return root;
}

WhiteWalnutApi.h

Arduino
#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

Credits

Walter Heger

Walter Heger

4 projects • 83 followers
Thanks to David Urbansky.

Comments