Как построить схему управления на Arduino и ESP8266 с настраиваемыми таймерами, контролируемую через Wi-Fi

Добавлено 27 августа 2017 в 15:05

В этой статье вы узнаете, как создать систему, которая может включать и выключать нагрузки постоянного тока с помощью мобильного приложения. Вы также узнаете, как выполнить эту задачу мгновенно или по таймерам, заранее установленным для включения и выключения нагрузок.

Обзор проекта

Вы можете реализовать эту систему там, где вам нужно включать нагрузку постоянного тока на определенное время. В этом вам поможет наше Android приложение, не требуя аппаратного интерфейса, клавиатуры и LCD дисплея.

Макет проекта
Макет проекта

Комплектующие

КомпонентКоличество
Arduino UNO или любая совместимая плата1
Wi-Fi модуль ESP82661
USB TTL конвертер (необязательно)1
Реле SPDT 12V2
Биполярный NPN транзистор BC3372
MOSFET транзистор с каналом N-типа BS1701
Резистор 1 кОм2
Резистор 10 кОм2
Диод 1N40072
Зажимные клеммы2
Настраиваемый регулятор напряжения LM3171
Конденсатор 0.1 мкФ2
Разъемы, мама и папа-
Пустая печатная плата2

Сборка макетной платы ESP8266

ESP8266 – недорогой SoC-чип со встроенным микроконтроллером и полным стеком протоколов TCP/IP, что означает, что он может напрямую обращаться к вашей Wi-Fi сети.

Поскольку у этого чипа есть свой микроконтроллер, вы можете поместить в него код своего приложения или можете использовать модуль просто как Wi-Fi приемопередатчик, что мы и собираемся сделать в данном проекте. Более эффективно было бы использовать этот модуль и как приемопередатчик, и как контроллер, но в целях обучения мы будем взаимодействовать с модулем, используя Arduino.

Чип ESP8266 поставляется в разных модулях. Мы будем использовать модуль ESP-01. Конечно, вы можете использовать любой другой модуль.

Во-первых, вы должны знать, что модуль работает с напряжением 3,3 В, и напряжение высокого логического уровня от Arduino должно быть таким же, чтобы не повредить наш модуль. Для этого требуется преобразователь уровня напряжения между платой Arduino (которая работает на 5 В) и модулем. Хорошей новостью является то, что в преобразователе будет нуждаться только вывод для передачи на Arduino, поскольку приемный вывод обычно распознает логические сигналы с напряжением 3,3 В от ESP8266.

Одним из простейших способов выполнения этого преобразования является схема от Sparkfun. Вы можете заказать готовый модуль.

Преобразователь уровня 5В – 3,3В
Преобразователь уровня 5В → 3,3В

На рисунке ниже показана распиновка нашего модуля на ESP8266:

Распиновка Wi-Fi модуля ESP8266 (вид сверху, не в масштабе)
Распиновка Wi-Fi модуля ESP8266 (вид сверху, не в масштабе)
ВыводНазначение
UTXDПередача данных через UART
URXDПрием данных через UART. Выход, к которому он подключается, должен быть 3,3 В.
CH_PDВыключение: низкий уровень на входе выключает чип, высокий уровень на входе включает его; для нормальной работы модуля необходимо подтянуть его к линии питания.
GPIO0При загрузке: должен быть высокий уровень, чтобы входить в нормальный режим загрузки; низкий уровень вводит в специальные режимы загрузки.
GPIO2При загрузке: низкий уровень заставляет загрузчик войти в режим загрузки флеш-памяти; высокий уровень вызывает нормальный режим загрузки.
RSTСброс; активный уровень – низкий.
GNDЗемля.
VCCПитание/3,3В.

Я использовал LM317, настраиваемый линейный регулятор напряжения с выходным током до 1,5 А, для обеспечения модуля подходящим источником питания 3,3 В.

Примечание: Не используйте вывод 3,3 В от Arduino, так как стабилизатор напряжения 3,3 В на плате Arduino не может обеспечить необходимую для модуля величину тока, особенно при пиковом потреблении энергии во время передачи.

Принципиальная схема макетной платы ESP8266
Принципиальная схема макетной платы ESP8266

Я использовал BS170 (вместо BSS138) для преобразователя логических уровней; оба работают хорошо.

Макетная плата ESP8266
Макетная плата ESP8266

Теперь вы можете подключить свой модуль к компьютеру, используя USB-TTL преобразователь, и испытать его.

Сборка макетной платы реле

Принципиальная схема макетной платы реле
Принципиальная схема макетной платы реле

Для управления реле я использовал биполярный NPN транзистор BC337 с резистором 1 кОм на базе. Для защиты от обратного напряжения катушки я использовал диод 1n4007.

Нормально замкнутый (NC) контакт реле я решил подключить к земле.

Макетная плата реле (вид сверху)
Макетная плата реле (вид сверху)
Макетная плата реле (вид снизу)
Макетная плата реле (вид снизу)

Код Arduino

Теперь мы сталкиваемся с проблемой. ESP8266 использует UART в качестве интерфейса для AT-команд, а Arduino Uno (которая использует Atmega328) имеет только один порт UART. Этот порт уже подключен к мосту USB-TTL, а также к выводам 0 и 1.

В качестве решения вы можете использовать эмулятор для UART порта на другом цифровом выводе Arduino с помощью библиотек AltSoftSerial или SoftwareSerial. Это позволит вам по-прежнему иметь аппаратный порт UART для отладки и печати сообщений в консоли, а программный порт – для связи с модулем.

Платы Arduino
Платы Arduino

Многие люди (включая меня) сообщают о проблемах с программным последовательным портом при высоких скоростях передачи – как на тех, что мы будем использовать с esp8266, 115200 бит/с. Я могу сказать, что у вас 50% принятых от модуля данных будет повреждено, если вы используете программный UART, а из переданных от Arduino к модулю данных почти 100% будет корректно. Я получил эти результаты после отслеживания сигналов на линиях RX и TX.

В качестве решения я добавил в код несколько директив define, чтобы облегчить вам выбор между аппаратным и программным UART портами. Имейте в виду, что вы не можете использовать один и тот же порт для отладки и общения с модулем, поэтому вам нужно выбирать между ними.

//раскомментируйте Serial.*** , если хотите использовать аппаратный последовательный порт (выводы 0,1)

//раскомментируйте esp8266.*** , если хотите использовать программный последовательный порт (выводы 2,3)

#define esp8266_Available() Serial.available() //esp8266.available()

#define esp8266_Find(ARG) Serial.find(ARG) //esp8266.find(ARG)

#define esp8266_Read() Serial.read() //esp8266.read()

#define esp8266_Write(ARG1,ARG2) Serial.write(ARG1,ARG2) //esp8266.write(ARG1,ARG2)

#define esp8266_Print(ARG) Serial.print(ARG) //esp8266.print(ARG)

В исходнике вы найдете часть кода, которая устанавливает модуля с вашим роутером:

sendCommand("AT+RST\r\n", 2000, DEBUG); // перезапустить модуль

sendCommand("AT+CWMODE=1\r\n", 1000, DEBUG); // настроить как точку доступа

sendCommand("AT+CWJAP=\"tur\",\"341983#tur\"\r\n", 3000, DEBUG); //**** ИЗМЕНИТЬ SSID и ПАРОЛЬ В СООТВЕТСТВИИ С ВАШЕЙ СЕТЬЮ ******//

delay(10000);

sendCommand("AT+CIFSR\r\n", 1000, DEBUG); // получить ip адрес

sendCommand("AT+CIPMUX=1\r\n", 1000, DEBUG); // настроить для нескольких соединений

sendCommand("AT+CIPSERVER=1,1337\r\n", 1000, DEBUG); // включить сервер на порту 1337

Цикл скетча ожидает команды, которые должны прийти через Wi-Fi соединение. В настоящее время поддерживаются следующие команды:

  • ‘con’ для получения состояния выводов, высокий или низкий логический уровень;
  • ‘on=’ включить соответствующий вывод;
  • ‘of=’ выключить соответствующий вывод;
  • ‘Tm=n/fS’ установить таймер включения (n) или выключения (f) соответствующего вывода.

Все команды имеют отклик подтверждения.

Примечания:

  • некоторые части скетча основаны на этом руководстве;
  • если вы используете модули со старым SDK, у вас могут быть такие же ошибки, как и у меня. Единственным решением в этом случае является обновление вашей прошивки до последней версии. Посмотрите эту статью, для получения помощи в обновлении прошивки модуля на ESP8266. Я обновил прошивку с версии 1.3 до 1.5.4.

Полный код программы:

#include <SoftwareSerial.h>

#define DEBUG 0 // если вы используете аппаратный последовательный порт, измените значение на 0 
#define ESPBaudRate 115200
#define HWSBaudRate 115200
#define OUTPUT1 11
#define OUTPUT2 12
#define OUTPUT3 13

//раскомментируйте Serial.*** , если хотите использовать аппаратный последовательный порт (выводы 0,1)
//раскомментируйте esp8266.*** , если хотите использовать программный последовательный порт (выводы 2,3)
#define esp8266_Available()      Serial.available()      //esp8266.available()
#define esp8266_Find(ARG)        Serial.find(ARG)        //esp8266.find(ARG)
#define esp8266_Read()           Serial.read()           //esp8266.read()
#define esp8266_Write(ARG1,ARG2) Serial.write(ARG1,ARG2) //esp8266.write(ARG1,ARG2)
#define esp8266_Print(ARG)       Serial.print(ARG)       //esp8266.print(ARG)

// Делает RX линию Arduino выводом 2, а TX линию Arduino выводом 3.
// Это означает, что вам необходимо подключить TX линию от ESP к выводу 2 Arduino,
// а RX линию от ESP к выводу 3 Arduino.
SoftwareSerial esp8266(2, 3);


/*************/
byte OUTPUTstate[3];
byte OUTPUTTMRIsSet[3] ;
byte OUTPUTTMRState[3] ;
long OUTPUTTimer[3];
/*************/

/***Commands**/
String GETSTATE = "con"; // Строка запроса от мобильного приложения, чтобы узнать состояние каждого выхода
String SETON = "on=";    // Строка запроса от мобильного приложения, чтобы включить выход
String SETOFF = "of=";   // Строка запроса от мобильного приложения, чтобы выключить выход
String TIMER = "tm=";    // Строка запроса от мобильного приложения, чтобы задать таймер для выхода
/*************/

void setup()
{
  Serial.begin(HWSBaudRate);  // Последовательный порт для отправки сообщений от Arduino на компьютер
  esp8266.begin(ESPBaudRate); // Программный последовательный порт для отправки сообщений от Arduino на ESP8266

  pinMode(OUTPUT1, OUTPUT);
  digitalWrite(OUTPUT1, LOW);

  pinMode(OUTPUT2, OUTPUT);
  digitalWrite(OUTPUT2, LOW);

  pinMode(OUTPUT3, OUTPUT);
  digitalWrite(OUTPUT3, LOW);

  // перезапустить модуль
  sendCommand("AT+RST\r\n", 2000, DEBUG); 
  // настроить как точку доступа
  sendCommand("AT+CWMODE=1\r\n", 1000, DEBUG); 
  //**** ИЗМЕНИТЬ SSID и ПАРОЛЬ В СООТВЕТСТВИИ С ВАШЕЙ СЕТЬЮ ******//
  sendCommand("AT+CWJAP=\"tur\",\"341983#tur\"\r\n", 3000, DEBUG); 
  delay(10000);
  // получить ip адрес
  sendCommand("AT+CIFSR\r\n", 1000, DEBUG); 
  // настроить для нескольких соединений
  sendCommand("AT+CIPMUX=1\r\n", 1000, DEBUG); 
  // включить сервер на порту 1337
  sendCommand("AT+CIPSERVER=1,1337\r\n", 1000, DEBUG); 
  if (DEBUG == true)
    Serial.println("Server Ready");
}

void loop()
{
  if (esp8266_Available()) // проверить, послал ли esp сообщение
  {

    if (esp8266_Find("+IPD,"))
    {
      // ждать, когда последовательный буфер заполнится (прочитаются все последовательные данные)  
      delay(1000); 

      // получить id подключения, чтобы мы могли отключиться
      int connectionId = esp8266_Read() - 48;
      // вычитаем 48 потому, что функция read() возвращает
      // десятичное значение в ASCII, а 0 (первое десятичное число) начинается с 48

      String closeCommand = "AT+CIPCLOSE="; // создание команды закрытия подключения
      closeCommand += connectionId;         // добавить id подключения
      closeCommand += "\r\n";

      esp8266_Find('?'); // Этот символ определяет начало команды теле нашего сообщения

      String InStream;
      InStream = (char) esp8266_Read();
      InStream += (char) esp8266_Read();
      InStream += (char) esp8266_Read();

      if (DEBUG == true)
        Serial.println(InStream);

      if (InStream.equals(GETSTATE))
      {
        // отклик на команду Status=<состояние_выхода_1><состояние_выхода_2><состояние_выхода_3>
        String response = "Status="; 
        response += OUTPUTstate[0];
        response += OUTPUTstate[1];
        response += OUTPUTstate[2];
        sendHTTPResponse(connectionId, response);
        sendCommand(closeCommand, 1000, DEBUG); // закрыть подключение
      }

      else if (InStream.equals(SETON))
      {
        int pinNumber = (esp8266_Read() - 48); // получить первую цифру, т.е., если вывод 13, то 1-ая цифра равна 1
        int secondNumber = (esp8266_Read() - 48);
        if (secondNumber >= 0 && secondNumber <= 9)
        {
          pinNumber *= 10;
          pinNumber += secondNumber; // получить вторую цифру, т.е., если вывод 13, то 2-ая цифра равна 3, 
                                     // и добавить ее к первой цифре
        }

        if (pinNumber == OUTPUT1)
          OUTPUTstate[0] = 1;
        else if (pinNumber == OUTPUT2)
          OUTPUTstate[1] = 1;
        else if (pinNumber == OUTPUT3)
          OUTPUTstate[2] = 1;

        digitalWrite(pinNumber, 1);

        String response = "Confg="; // Отклик на команду Confg=<номер_вывода>
        response += pinNumber;
        sendHTTPResponse(connectionId, response);
        sendCommand(closeCommand, 1000, DEBUG); // закрыть подключение
      }

      else if (InStream.equals(SETOFF))
      {
        int pinNumber = (esp8266_Read() - 48); // получить первую цифру, т.е., если вывод 13, то 1-ая цифра равна 1
        int secondNumber = (esp8266_Read() - 48);
        if (secondNumber >= 0 && secondNumber <= 9)
        {
          pinNumber *= 10;
          pinNumber += secondNumber; // получить вторую цифру, т.е., если вывод 13, то 2-ая цифра равна 3, 
                                     // и добавить ее к первой цифре
        }

        if (pinNumber == OUTPUT1)
          OUTPUTstate[0] = 0;
        else if (pinNumber == OUTPUT2)
          OUTPUTstate[1] = 0;
        else if (pinNumber == OUTPUT3)
          OUTPUTstate[2] = 0;

        digitalWrite(pinNumber, 0); // изменить состояние вывода

        String response = "Confg="; // Отклик на команду Confg=<номер_вывода>
        response += pinNumber;
        sendHTTPResponse(connectionId, response);
        sendCommand(closeCommand, 1000, DEBUG); // закрыть подключение
      }

      else if (InStream.equals(TIMER))
      {
        int pinNumber = (esp8266_Read() - 48); // получить первую цифру, т.е., если вывод 13, то 1-ая цифра равна 1
        int secondNumber = (esp8266_Read() - 48);
        if (secondNumber >= 0 && secondNumber <= 9)
        {
          pinNumber *= 10;
          pinNumber += secondNumber; // получить вторую цифру, т.е., если вывод 13, то 2-ая цифра равна 3, 
                                     // и добавить ее к первой цифре
        }

        if (esp8266_Read() == 'n')
        {
          if (DEBUG == true)
            Serial.println("on");

          if (pinNumber == OUTPUT1)
            OUTPUTTMRState[0] = 1;
          else if (pinNumber == OUTPUT2)
            OUTPUTTMRState[1] = 1;
          else if (pinNumber == OUTPUT3)
            OUTPUTTMRState[2] = 1;
        }
        else
        {
          if (DEBUG == true)
            Serial.println("off");

          if (pinNumber == OUTPUT1)
            OUTPUTTMRState[0] =  0;
          else if (pinNumber == OUTPUT2)
            OUTPUTTMRState[1] = 0;
          else if (pinNumber == OUTPUT3)
            OUTPUTTMRState[2] = 0;
        }

        int j = 0;
        byte Atime[5]; // Таймер может настроен на максимальное значение в 1 сутки
                       // поэтому программа может принять 5 цифр, так как 1 сутки равны 86400 секундам
        long Time;

        // Прочитать секунды, значение имеет переменное количество цифр, поэтому читать, пока не получим 's',
        // что является символом завершения в теле моего сообщения от мобильного телефона
        while (1) 
        {
          Time = esp8266_Read();
          if (Time == 's')
            break;
          Atime[j] = Time - 48 ;
          j++;
        }

        switch (j) // секунды ...
        {
          case 1: // одна цифра
            Time = Atime[0];
            break;
          case 2: // две цифры
            Time = Atime[0] * 10 + Atime[1];
            break;
          case 3: // три цифры
            Time = Atime[0] * 100 + Atime[1] * 10 + Atime[2];
            break;
          case 4: // четыре цифры
            Time = Atime[0] * 1000 + Atime[1] * 100 + Atime[2] * 10 + Atime[3];
            break;
          case 5: // пять цифр
            Time = Atime[0] * 10000 + Atime[1] * 1000 + Atime[2] * 100 + Atime[3] * 10 + Atime[j];
            break;
        }

        if (DEBUG == true)
        {
          Serial.println("Timer:");
          Serial.println(Time);
        }

        Time = Time * 1000 + millis();

        if (DEBUG == true)
        {
          Serial.println("Pin:");
          Serial.println(pinNumber);
        }

        if (pinNumber == OUTPUT1)
        {
          OUTPUTTMRIsSet[0] = 1;
          OUTPUTTimer[0] = Time;
        }
        else if (pinNumber == OUTPUT2)
        {
          OUTPUTTMRIsSet[1] = 1;
          OUTPUTTimer[1] = Time;
        }
        else if (pinNumber == OUTPUT3)
        {
          OUTPUTTMRIsSet[2] = 1;
          OUTPUTTimer[2] = Time;
        }
        String response = "tConfg="; // Отклик на команду tConfg=<номер_вывода>
        response += pinNumber;

        sendHTTPResponse(connectionId, response);
        sendCommand(closeCommand, 1000, DEBUG); // закрыть подключение
      }

      else // принята неподдерживаемая команда
      {
        String response = "ERROR";
        sendHTTPResponse(connectionId, response);
        sendCommand(closeCommand, 1000, DEBUG); // закрыть подключение
      }

    }
  }

  /*****Проверить таймер для каждого выхода******/
  if (OUTPUTTMRIsSet[0] != 0 && (OUTPUTTimer[0] < millis()))
  {
    digitalWrite(OUTPUT1, OUTPUTTMRState[0]);
    OUTPUTstate[0] = OUTPUTTMRState[0];
    OUTPUTTMRIsSet[0] = 0;
  }

  if (OUTPUTTMRIsSet[1] != 0 && (OUTPUTTimer[1] < millis()))
  {
    digitalWrite(OUTPUT2, OUTPUTTMRState[1]);
    OUTPUTstate[1] = OUTPUTTMRState[1];
    OUTPUTTMRIsSet[1] = 0;
  }

  if (OUTPUTTMRIsSet[2] != 0 && (OUTPUTTimer[2] < millis()))
  {
    digitalWrite(OUTPUT3, OUTPUTTMRState[2]);
    OUTPUTstate[2] = OUTPUTTMRState[2];
    OUTPUTTMRIsSet[2] = 0;
  }
  /***************************************/

}

/*
  Name: sendData
  Description: Функция, используемая для отправки данных на ESP8266.
  Params: command - данные/команда для отправки; timeout - время ожидания отклика; debug - печатать в консоль?(true = да, false = нет)
  Returns: Отклик от esp8266 (если есть отклик)
*/
String sendData(String command, const int timeout, boolean debug)
{
  String response = "";

  int dataSize = command.length();
  char data[dataSize];
  command.toCharArray(data, dataSize);

  esp8266_Write(data, dataSize); // передача символов на esp8266
  if (debug)
  {
    Serial.println("\r\n====== HTTP Response From Arduino ======");
    Serial.write(data, dataSize);
    Serial.println("\r\n========================================");
  }

  long int time = millis();

  while ( (time + timeout) > millis())
  {
    while (esp8266_Available())
    {

      // У esp есть данные, поэтому вывести их в консоль
      char c = esp8266_Read(); // прочитать следующий символ.
      response += c;
    }
  }

  if (debug)
  {
    Serial.print(response);
  }

  return response;
}

/*
  Name: sendHTTPResponse
  Description: Функция, которая посылает HTTP 200, HTML UTF-8 отклик
*/
void sendHTTPResponse(int connectionId, String content)
{

  // создать HTTP отклик
  String httpResponse;
  String httpHeader;
  // HTTP заголовок
  httpHeader = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n";
  httpHeader += "Content-Length: ";
  httpHeader += content.length();
  httpHeader += "\r\n";
  httpHeader += "Connection: close\r\n\r\n";
  httpResponse = httpHeader + content + " "; // Здесь в коде баг: последний символ в "content" не посылается, поэтому я добавил дополнительный пробел
  sendCIPData(connectionId, httpResponse);
}

/*
  Name: sendCIPDATA
  Description: посылает команду CIPSEND=<id_подключения>,<данные>

*/
void sendCIPData(int connectionId, String data)
{
  String cipSend = "AT+CIPSEND=";
  cipSend += connectionId;
  cipSend += ",";
  cipSend += data.length();
  cipSend += "\r\n";
  sendCommand(cipSend, 1000, DEBUG);
  sendData(data, 1000, DEBUG);
}

/*
  Name: sendCommand
  Description: Функция, используемая для отправки данных на ESP8266.
  Params: command - данные/команда для отправки; timeout - время ожидания отклика; debug - печатать в консоль?(true = да, false = нет)
  Returns: Отклик от esp8266 (если есть отклик)
*/
String sendCommand(String command, const int timeout, boolean debug)
{
  String response = "";

  esp8266_Print(command); // передача символов на esp8266

  long int time = millis();

  while ( (time + timeout) > millis())
  {
    while (esp8266_Available())
    {

      // У esp есть данные, поэтому вывести их в консоль
      char c = esp8266_Read(); // прочитать следующий символ.
      response += c;
    }
  }

  if (debug)
  {
    Serial.print(response);
  }

  return response;
}

Android приложение

Чтобы управлять всеми выше перечисленными аппаратными компонентами, мы будем использовать простое приложение для Android. Это приложение позволит нам включать или выключать выход напрямую или через определенный период времени.

Примечание: Приложение требует Android 4.0 (IceCreamSandwich) или выше.

  • Прежде всего, вы должны знать IP адрес своего модуля. Если вы использовали программный последовательный порт, IP адрес будет напечатан в консоли. Если вы использовали аппаратный последовательный порт, то вы должны использовать кабель для отслеживания данных на линиях RX и TX, чтобы увидеть IP адрес. Вам также нужно знать номер порта, который был указан в скетче для Arduino. После этого нажмите "connect", чтобы получить состояние всех трех выходов. Вам нужно убедиться, что ваш Wi-Fi роутер включен, и вы подключены к локальной сети.
  • Теперь нажмите на любой переключатель, который вы хотите включить/выключить. Всякий раз, когда захотите, вы можете нажать "refresh", чтобы обновить состояние всех выходов.
  • На вкладке "Timers" вы можете установить любой из этих трех выходов для включения/выключения через определенный промежуток времени (от 0 до 24 часов).
  • После любого действия вы получите сообщение с подтверждением о том, выполнилась ли команда успешно, или возникла какая-то ошибка.
Скриншоты Android приложения для управления контроллером на Arduino и ESP8266
Скриншоты Android приложения для управления контроллером на Arduino и ESP8266

Демонстрационное видео

Вот и всё! Надеюсь, статья оказалась полезной. Оставляйте комментарии!


На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.


Сообщить об ошибке