Arduino и ESP8266: веб-сервер

Это более старое руководство (изначально опубликовано 4 января 2015 г., обновлено 7 июля 2017 г.), демонстрирующее создание веб-сервера с использованием Arduino и модуля ESP8266 через AT-команды.

Примечание

Это очень старое руководство, и с тех пор многое изменилось. Одним из главных достижений стало ядро ESP8266 для Arduino IDE, позволяющее программировать ESP8266 напрямую.


Веб-страница ESP8266

Веб-страница с введённым именем

Функциональность

Веб-сервер включает:

  • Счётчик запросов

  • Текстовое поле ввода

  • Отправку имени через HTTP GET запрос


Используемые AT-команды

  • AT+CWMODE=1 – режим станции (ESP8266 подключается как клиент к существующей сети)

  • AT+CWJAP=»NetworkSSID»,»password» – подключение к сети с учётными данными

  • AT+CIFSR – возвращает IP-адрес, выделенный роутером

  • AT+CIPMUX=1 – включение множественных одновременных подключений (необходимо для режима сервера)

  • AT+CIPSERVER=1,80 – запуск сервера на порту 80

  • AT+CIPSEND=0,25 – отправка 25 символов на канале 0

  • AT+CIPCLOSE=0 – закрытие соединения на канале 0


Полный скетч Arduino

// Базовый веб-сервер Arduino и ESP8266
//
// использует AltSoftSerial, скачать с https://www.pjrc.com/teensy/td_libs_AltSoftSerial.html
// можно заменить на обычный Software Serial

#include <AltSoftSerial.h>
// Arduino пин 08 для RX
// Arduino пин 09 для TX

AltSoftSerial espSerial;

const bool printReply = true;
const char line[] = "-----\n\r";
int loopCount=0;

char html[50];
char command[20];
char reply[500]; // обычно так не делают

char ipAddress [20];
char name[30];
int lenHtml = 0;
char temp[5];

void setup()
{
      Serial.begin(9600);
      Serial.println("Start\r\n\r\n");

      espSerial.begin(9600); // скорость вашего модуля ESP8266 может отличаться

      // сброс ESP8266
      Serial.println("reset the module");
      espSerial.print("AT+RST\r\n");
      getReply( 2000 );

      // настройка как станция
      Serial.println("Change to station mode");
      espSerial.print("AT+CWMODE=1\r\n");
      getReply( 1500 );

      // подключение к сети. Используется DHCP. IP будет назначен роутером.
      Serial.println("Connect to a network ");

     // Введите SSID и пароль вашей сети
      espSerial.print("AT+CWJAP=\"myNetworkSSID\",\"password\"\r\n");
      getReply( 6000 );

      // получение IP-адреса
      Serial.println("Get the ip address assigned by the router");
      espSerial.print("AT+CIFSR\r\n");
      getReply( 1000 );

      // разбор IP-адреса
      int len = strlen( reply );
      bool done=false;
      bool error = false;
      int pos = 0;
      while (!done)
      {
           if ( reply[pos] == 10) { done = true;}
           pos++;
           if (pos > len) { done = true;  error = true;}
      }

      if (!error)
      {
            int buffpos = 0;
            done = false;
            while (!done)
            {
               if ( reply[pos] == 13 ) { done = true; }
               else { ipAddress[buffpos] = reply[pos];    buffpos++; pos++;   }
            }
            ipAddress[buffpos] = 0;
      }
      else { strcpy(ipAddress,"ERROR"); }

      // настройка множественных подключений
      Serial.println("Set for multiple connections");
      espSerial.print("AT+CIPMUX=1\r\n");
      getReply( 1500 );

      // запуск сервера на порту 80
      Serial.println("Start the server");
      espSerial.print("AT+CIPSERVER=1,80\r\n");
      getReply( 1500 );

      Serial.println("");

      Serial.println("Waiting for page request");
      Serial.print("Connect to "); Serial.println(ipAddress);
      Serial.println("");
}

void loop()
{
      if(espSerial.available()) // проверка, отправляет ли ESP8266 данные
      {
          // это ответ +IPD -- он довольно длинный
          // обычно не нужно копировать всё сообщение в переменную
          getReply( 2000 );

          bool foundIPD = false;
          for (int i=0; i<strlen(reply); i++)
          {
               if (  (reply[i]=='I') && (reply[i+1]=='P') && (reply[i+2]=='D')   ) { foundIPD = true;    }
          }

          if ( foundIPD  )
          {

              loopCount++;

              // проверка наличия имени -- ищем name=
              bool haveName = false;
              int nameStartPos = 0;
              for (int i=0; i<strlen(reply); i++)
              {
                   if (!haveName)
                   {
                         if (  (reply[i]=='n') && (reply[i+1]=='a') && (reply[i+2]=='m') && (reply[i+3]=='e')  && (reply[i+4]=='=') )
                         {
                             haveName = true;
                             nameStartPos = i+5;
                         }
                   }
              }

              // получение имени -- копируем всё от nameStartPos до первого пробела
              if (haveName)
              {
                    int tempPos = 0;
                    bool finishedCopying = false;
                    for (int i=nameStartPos; i<strlen(reply); i++)
                    {
                         if ( (reply[i]==' ') && !finishedCopying )  { finishedCopying = true;   }
                         if ( !finishedCopying )                     { name[tempPos] = reply[i];   tempPos++; }
                    }
                    name[tempPos] = 0;
              }

              if (haveName) { Serial.print( "name = ");  Serial.println(name); Serial.println(""); }
              else          { Serial.println( "no name entered");   Serial.println("");           }

              // начинаем отправку HTML

              strcpy(html,"<html><head></head><body>");
              strcpy(command,"AT+CIPSEND=0,25\r\n");
              espSerial.print(command);
              getReply( 2000 );
              espSerial.print(html);
              getReply( 2000 );

              strcpy(html,"<h1>ESP8266 Webserver</h1>");
              strcpy(command,"AT+CIPSEND=0,26\r\n");
              espSerial.print(command);
              getReply( 2000 );
              espSerial.print(html);
              getReply( 2000 );

              strcpy(html,"<p>Served by Arduino and ESP8266</p>");
              strcpy(command,"AT+CIPSEND=0,36\r\n");
              espSerial.print(command);
              getReply( 2000 );
              espSerial.print(html);
              getReply( 2000 );

              strcpy(html,"<p>Request number ");
              itoa( loopCount, temp, 10);
              strcat(html,temp);
              strcat(html,"</p>");

              // нужна длина html
              int lenHtml = strlen( html );

              strcpy(command,"AT+CIPSEND=0,");
              itoa( lenHtml, temp, 10);
              strcat(command, temp);
              strcat(command, "\r\n");
              espSerial.print(command);
              getReply( 2000 );
              espSerial.print(html);
              getReply( 2000 );

             if (haveName)
             {
                  // вывод имени
                  strcpy(html,"<p>Your name is "); strcat(html, name ); strcat(html,"</p>");

                  // нужна длина html для команды cipsend
                  lenHtml = strlen( html );
                  strcpy(command,"AT+CIPSEND=0,"); itoa( lenHtml, temp, 10); strcat(command, temp); strcat(command, "\r\n");
                  espSerial.print(command);
                  getReply( 2000 );
                  espSerial.print(html);
                  getReply( 2000 );
             }

              strcpy(html,"<form action=\""); strcat(html, ipAddress); strcat(html, "\" method=\"GET\">");

              lenHtml = strlen( html );
              itoa( lenHtml, temp, 10);
              strcpy(command,"AT+CIPSEND=0,");
              itoa( lenHtml, temp, 10);
              strcat(command, temp);
              strcat(command, "\r\n");

              espSerial.print(command);
              getReply( 2000 );
              espSerial.print(html);
              getReply( 2000 );

              strcpy(html,"Name:<br><input type=\"text\" name=\"name\">");
              strcpy(command,"AT+CIPSEND=0,40\r\n");
              espSerial.print(command);
              getReply( 2000 );
              espSerial.print(html);
              getReply( 2000 );

              strcpy(html,"<input type=\"submit\" value=\"Submit\"></form>");
              strcpy(command,"AT+CIPSEND=0,43\r\n");
              espSerial.print(command);
              getReply( 2000 );
              espSerial.print(html);
              getReply( 2000 );

              strcpy(html,"</body></html>");
              strcpy(command,"AT+CIPSEND=0,14\r\n");
              espSerial.print(command);
              getReply( 2000 );
              espSerial.print(html);
              getReply( 2000 );

              // закрытие соединения
              espSerial.print( "AT+CIPCLOSE=0\r\n" );
              getReply( 1500 );

              Serial.println("last getReply 1 ");
              getReply( 1500 );

              Serial.println("last getReply 2 ");
              getReply( 1500 );

          } // if(espSerial.find("+IPD"))
      } //if(espSerial.available())

      delay (100);

      // ожидание следующего запроса

}

void getReply(int wait)
{
    int tempPos = 0;
    long int time = millis();
    while( (time + wait) > millis())
    {
        while(espSerial.available())
        {
            char c = espSerial.read();
            if (tempPos < 500) { reply[tempPos] = c; tempPos++;   }
        }
        reply[tempPos] = 0;
    }

    if (printReply) { Serial.println( reply );  Serial.println(line);     }
}

Настройка

Пользователям необходимо изменить учётные данные сети, заменив SSID и пароль в строке espSerial.print на свои реальные данные WiFi.


Известные проблемы

  • Длительное время отклика из-за 2-секундных задержек на команды

  • Модуль иногда возвращает ответы «busy», требующие повторной отправки

  • Лишние сообщения от ESP8266 после передачи страницы

  • Автор признаёт, что код недостаточно эффективен и мог бы выиграть от рефакторинга