- Управление Web - сервером на Arduino Mega Android - устройством


Управление через Интернет голосовыми командами Web - сервером на Arduino Mega и контроллере W5100 c файлами на SD 
Практика для студентов 
Мясищев А.А.

      Экспериментально было обнаружено, что web - сервер, построенный на Ардуино (микроконтроллеры ATmega) с Ethernet Shield W5100 зависает примерно через 10-15 минут при интенсивном обращении к нему через Интернет. Было установлено, что это связанно с особенностью работы контроллера W5100 и библиотеки Ethernet Arduino IDE. 
В связи с этим здесь поставлены следующие задачи:
1. На контроллере Arduino Mega (1280 или 2560), Ethernet Shield W5100 построить устойчиво работающий Web - server, в котором html файлы хранятся на SD - карте памяти.
        

2. При подключении к серверу с помощью браузера должна появиться следующая форма управления:
Сервер должен получать данные с температурного датчика DS18B20, звонить на мобильный телефон на заданный номер (например, в случае срабатывания концевого выключателя входной двери - реализация охранной системы), включать и выключать одно устройство. Сервисными функциями являются: установка времени на микроконтроллере (сервере), установка времени ежесуточного дозвона (сигнал об исправности системы), установка времени принудительного отключения оборудования (например, связь с Интернет прервалась, но оборудование необходимо выключить). Должна также высвечиваться информация о состоянии устройства (включено или выключено).
3. Возможность управления сервером с помощью приложения на Андроид устройстве. Управление должно быть кнопочным и с помощью голосовых команд. Приложение должно иметь возможность запоминать два адреса сервера: глобальный и локальный в энергонезависимой памяти. Глобальный используется для удаленного управления через Интернет, локальный - в пределах дома, офиса. Должны поддерживаться следующие кнопочные и голосовые команды: включить, выключить, температура, состояние оборудования. В программе на Андроид показана также возможность организации простейшего диалога - реализованы ответы на вопросы: "Кто ты", "твои команды", "Откуда ты". При выполнении очередной команды синтезатор голоса должен оповещать о выполненной команде.


На следующем рисунке показана схема соединений:

Программа для Arduino ATMega1280 (Здесь использован Arduino IDE ver.1.0.5)
//Arduino ATMega1280
#include <SPI.h>
#include <SD.h>
#include <string.h>
#include <stdio.h>
#include <Ethernet.h>
#include <OneWire.h>                  //Подключаем описание библиотеки шины OneWire
#include <DallasTemperature.h>        //Подключаем описание библиотеки для температуры(DS18B20)
#define FORM "<FORM action=\"\" >"
#define ONE_WIRE_BUS 6
OneWire oneWire(ONE_WIRE_BUS);       //Настройка шины для работы с 6-м выводом Ардуино
DallasTemperature sensors(&oneWire); //Подключаем датчик температуры
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192,168,1,32);
char rootFileName[] = "index.htm";
EthernetServer server(82);
File myFile; 
int clo=0;
int sec=0, mi=0, ho=0, mi1=0, ho1=10, mi2=30, ho2=23, sm=0, tel=0, dv=0, ob=0; 
char fh[3],fh1[3],fh2[3];
char fm[3],fm1[3],fm2[3];
  float celsius=0;
void setup() {
  pinMode(2,INPUT); // Сигнализация
  digitalWrite(2, HIGH); // Сигнализация
  pinMode(5,OUTPUT);   // Telephone
  digitalWrite(5, LOW); // Telephone
  pinMode(7,OUTPUT);   // Освещение
  digitalWrite(7, LOW); // Освещение  
  pinMode(30,OUTPUT);
  digitalWrite(30, LOW);
  
  sensors.begin();//Инициализация датчика температуры DS18B20
  Serial.begin(38400);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);  
  Ethernet.begin(mac, ip, gateway, subnet);
  server.begin();
  SD.begin(4);
  CLKPR=0x80;CLKPR=0x00;
  TCCR5A=0x00 ;TCCR5B=0x0C;//Делитель тактовой частоты счетчика 
  
  TCNT5L=0x00;TCNT5H=0x00;   ICR5L=0x00; ICR5H=0x00;
  OCR5AH=0xF4;OCR5AL=0x1B; OCR5BH=0x00; OCR5BL=0x00;OCR5CH=0x00; OCR5CL=0x00; 
 TIMSK5=0x02; // Прерывание при совпадении с A 
}

ISR (TIMER5_COMPA_vect)
{
  sec++;
  if(sec==60) {mi++; sec=0;}
  if(mi==60) {ho++; mi=0; sec=0;}
  if(ho==24) {ho=0; mi=0; sec=0;}
/* if(sm==10){ sm=0;}
 if (sm==0)digitalWrite(30,HIGH);
 if (sm==1)digitalWrite(30,LOW);
 sm++; */
}

// How big our line buffer should be. 100 is plenty!
#define BUFSIZ 100

void loop() 
{
  int soc0=0,soc1=0,soc2=0,soc3=0,len0=0,len1=0,len2=0,len3=0;
  char clientline[BUFSIZ];
  char *filename;
  char android[256];
  char andrbuf1[30];
  char strok[30];

  int index = 0; 
  
     if(digitalRead(2)==LOW && dv==0) //Проверяем, сработал ли концевой выключатель на 2-м выводе
  {
    delay(500);   // Даем задержку
    if(digitalRead(2)==LOW && dv==0) // И еще раз проверяем срабатывание концевого выключателя
    {
          digitalWrite(5, HIGH);  delay(100); digitalWrite(5, LOW); // Звоним на мобильник, если дверь открыта
          delay(200);digitalWrite(5, HIGH);delay(100); digitalWrite(5, LOW);  delay(1000); dv=1;
    }
  }
  
  if(digitalRead(2)==HIGH) dv=0;
  
      if(ho==ho1 && mi==mi1 && tel==0) // Звоним при совпадении времени
    {
      tel=1;
          digitalWrite(5, HIGH);  delay(100); digitalWrite(5, LOW); // Звоним на мобильник, если дверь открыта
          delay(200);digitalWrite(5, HIGH);delay(100); digitalWrite(5, LOW);  delay(600);
    }  
    if( ( (ho != ho1) || (mi != mi1) ) && tel==1 ) tel=0;
     if(ho==ho2 && mi==mi2 && ob==0) // При совпадении времени отключаем оборудование 
     { ob=1; digitalWrite(7,LOW);  }
     if( ( (ho != ho2) || (mi != mi2) ) && ob==1 ) ob=0;
     
 aa:
 int si=0; 
 EthernetClient client; 
 client = server.available(); // server.available() - открытие socket и его прослушивание 
  
  soc0=client.sockstat(0);soc1=client.sockstat(1);soc2=client.sockstat(2);soc3=client.sockstat(3);
  len0=client.getlen(0);len1=client.getlen(1);len2=client.getlen(2);len3=client.getlen(3);
  if (soc0==0x17 ||soc1 ==0x17 ||soc2==0x17 ||soc3==0x17  ) // Если соединение есть (ESTABLISHMENT) то определяем
  // длину поля данных пакета TCP. Если эта длина для активного socket 15 раз равна 0, то срочно закрываем соединение
  // иначе сервер "зависнет". Причем зависание происходит при подключении через Интернет. По всей видимости пакеты 
  // на закрытие соединения теряются, соединение остается открытым, открытыми остается много соединений и их надо
  // принудительно закрывать со стороны сервера. 
  { 
    delay(5);
    if (soc0==0x17) {si=0; goto ss;} if (soc1==0x17) {si=1; goto ss;} 
    if (soc2==0x17) {si=2; goto ss;}
    if (soc3==0x17) si=3;
 ss:   
    Serial.print("begin, si =");Serial.print(si);Serial.print(" Sock="); Serial.println(client.sock1());
 
  Serial.print("Length="); Serial.print(len0); Serial.print(" ");Serial.print(len1);Serial.print(" ");
  Serial.print(len2);Serial.print(" ");Serial.print(len3);
  Serial.print(" Length.availble="); Serial.print(client.available());
    Serial.print(" clo= ");Serial.print(clo);Serial.print(" stat="); 
   Serial.print(soc0,HEX);Serial.print(" ");Serial.print(soc1,HEX);Serial.print(" ");
  Serial.print(soc2,HEX);Serial.print(" ");Serial.println(soc3,HEX);
  Serial.print(" status=");  Serial.println(client.status(),HEX);
  if ((clo==15) && (client.available() == 0 ))//Если соединение не закрыто клиентом, интенсивно закрываем его сервером
    { clo=0; delay(10);
    client.disc(0); client.disc(1); client.disc(2); client.disc(3);delay(50);client.clos(0);client.clos(1);
    client.clos(2);client.clos(3);
  goto aa;} // Опять открываем  socket и начинаем его прослушивание
  
    clo++; 
   if (client.available() > 0) clo=0;  
  
    if (client) {
    index = 0;  
while (client.connected()){ 
        if (client.available()) {
          char c = client.read(); 
        // If it isn't a new line, add the character to the buffer
        if (c != '\n' && c != '\r') {
          clientline[index] = c;
          index++;
          // continue to read more data!
          continue;
        }        
        clientline[index] = 0;
         // Print it out for debugging
            Serial.println(clientline);
            
        if((strstr(clientline, "GET /temp?t=1" ) || strstr(clientline, "GET /andr?t=1" )) !=0) {sensors.requestTemperatures();
             } 
        if(strstr(clientline, "GET /temp?z=136111a") !=0)
            {digitalWrite(5, HIGH);  delay(100); digitalWrite(5, LOW);
             delay(200);digitalWrite(5, HIGH);delay(100); digitalWrite(5, LOW);} 
        if((strstr(clientline, "GET /temp?d=1361111") || strstr(clientline, "GET /andr?d=1361111"))!=0) { digitalWrite(7,HIGH); }  
        if((strstr(clientline, "GET /temp?d=1361110") || strstr(clientline, "GET /andr?d=1361110"))!=0) { digitalWrite(7,LOW); }  
        
        if(strstr(clientline, "GET /temp?c=111") !=0)
             { 
    fh[0]=clientline[15]; fh[1]=clientline[16]; fh[2]=0;
    fm[0]=clientline[17]; fm[1]=clientline[18]; fm[2]=0;
    ho=atoi(fh); mi=atoi(fm);
                }
                if(strstr(clientline, "GET /temp?v=112") !=0)
             { 
    fh1[0]=clientline[15]; fh1[1]=clientline[16]; fh1[2]=0;
    fm1[0]=clientline[17]; fm1[1]=clientline[18]; fm1[2]=0;
    ho1=atoi(fh1); mi1=atoi(fm1);
                }
                if(strstr(clientline, "GET /temp?y=113") !=0)
             { 
    fh2[0]=clientline[15]; fh2[1]=clientline[16]; fh2[2]=0;
    fm2[0]=clientline[17]; fm2[1]=clientline[18]; fm2[2]=0;
    ho2=atoi(fh2); mi2=atoi(fm2);
                }
    // Если запрос  http://ipaddr:port/andr распечатывается температура и остояние оборудования
            if (strstr(clientline, "GET /andr") != 0) {
         strcpy((char *)android,"HTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\r\n\r\n");
         celsius=sensors.getTempCByIndex(0);
         dtostrf(celsius,5,2,andrbuf1);
         sprintf(strok,"<h1>Температура = %s град.  ",andrbuf1); 
         strcat((char *)android,(char *)strok);     
              if (digitalRead(7)){
         strcat((char *)android,"  Оборудование включено </h1>");                 
        }
      else { 
        strcat((char *)android,"  Оборудование выключено</h1>"); 
            }
            // Output client            
byte cB[256];
int ii=0;
  int cC=0;    
    while (ii<strlen((char *)android)) 
       {
         cB[cC]=android[ii];
         cC++;
         Serial.println(android[ii]);
       if(cC > 255)
        {
         client.write(cB,256);
        cC=0; 
        } 
        ii++;
       } 
      if(cC > 0) 
      {     
        client.write(cB,cC);    
      }     
break;     
    }           
         // Если запрос http://ipaddr:port/temp распечатываем на браузере команды
          if (strstr(clientline, "GET /temp") != 0) {client.println("HTTP/1.1 200 OK");
// Температура, время
          client.println("Content-Type: text/html; charset=utf-8");
          client.println();         
          client.print("<b>Температура, время:</b><BR>");
          client.print(FORM);
          client.print("<INPUT type=\"HIDDEN\" name=\"t\" value=\"1\" size=2>");
          client.print("<INPUT type=\"submit\" value=\"Показать\"> </FORM>");
          client.print("Температура= "); 
          client.print(sensors.getTempCByIndex(0)); client.print(" град.<BR>"); 
client.print("Время на микроконтроллере= "); if(ho/10==0) client.print("0"); client.print(ho);  client.print(":"); 
if(mi/10==0) client.print("0"); client.print(mi);  client.print(" час.<BR>");  

client.print("Время дозвона= "); if(ho1/10==0) client.print("0"); client.print(ho1);  client.print(":"); 
if(mi1/10==0) client.print("0"); client.print(mi1);  client.print(" час.<BR>"); 

client.print("Время отключения= "); if(ho2/10==0) client.print("0"); client.print(ho2);  client.print(":"); 
if(mi2/10==0) client.print("0"); client.print(mi2);  client.print(" час.<BR>"); 
// Звонок по телефону       
          client.print("<br><b>Для дозвона введи пароль!</b><BR>");
          client.print(FORM);
          client.print("<INPUT type=\"PASSWORD\" name=\"z\" value=\"\" size=10>");
          client.print("<INPUT type=\"submit\" value=\"Звонить\"> </FORM>");   
// Установка времени      
          client.print("<b>Установка времени:</b><BR>");
          client.print("Формат: xxx0925 <BR>");
          client.print("xxx-код, 09-часы, 25-минуты <BR>");
          client.print(FORM);
          client.print("<INPUT type=\"PASSWORD\" name=\"c\" value=\"\" size=10>");
          client.print("<INPUT type=\"submit\" value=\"Установить\"> </FORM>"); 
// Установка времени дозвона      
          client.print("<b>Установка времени дозвона:</b><BR>");
          client.print(FORM);
          client.print("<INPUT type=\"PASSWORD\" name=\"v\" value=\"\" size=10>");
          client.print("<INPUT type=\"submit\" value=\"Установить\"> </FORM>");   
// Установка времени отключения      
          client.print("<b>Установка времени отключения:</b><BR>");
          client.print(FORM);
          client.print("<INPUT type=\"PASSWORD\" name=\"y\" value=\"\" size=10>");
          client.print("<INPUT type=\"submit\" value=\"Установить\"> </FORM>");   
// Формирование  формы «Управление  оборудованием»         
          client.print("<b>Управление оборудованием:</b><BR>");
          client.print(FORM);
          client.print("<INPUT type=\"PASSWORD\" name=\"d\" value=\"\" size=10>");
          client.print("<INPUT type=\"submit\" value=\"Включить\"> </FORM>");
          client.print(FORM);
          client.print("<INPUT type=\"PASSWORD\" name=\"d\" value=\"\" size=10>");
          client.print("<INPUT type=\"submit\" value=\"Выключить\"> </FORM>");
         if (digitalRead(7)){
        client.print("Оборудование включено<br><br>");
        }
      else { client.print("Оборудование выключено<br><br>");  }
         client.print(FORM);
         client.print("<INPUT type=\"HIDDEN\" name=\"h\" value=\"0\" size=2>");
         client.print("<a href=\"/\">Перейти к главной странице</a>");     
 // Выходим с цикла while
          break;
          }
          
        filename = 0;       
       
        // Look for substring such as a request to get the root file
        if (strstr(clientline, "GET / ") != 0) {
          filename = rootFileName;
        }
        if (strstr(clientline, "GET /") != 0) {
          // this time no space after the /, so a sub-file         
          if (!filename) filename = clientline + 5; // look after the "GET /" (5 chars)
          // a little trick, look for the " HTTP/1.1" string and
          // turn the first character of the substring into a 0 to clear it out.
          (strstr(clientline, " HTTP"))[0] = 0;
         
 //        print the file we want
       Serial.println(filename);  
     myFile = SD.open(filename);
        if (!myFile ) {
            client.println("HTTP/1.1 404 Not Found");
            client.println("Content-Type: text/html");
            client.println();
            client.println("<h2>File Not Found!</h2>");
            break;
          }          
       //   Serial.println("Opened!");
         
          client.println("HTTP/1.1 200 OK");
          if (strstr(filename, ".htm") != 0)
             client.println("Content-Type: text/html; charset=windows-1251");
         else if (strstr(filename, ".css") != 0)
             client.println("Content-Type: text/css");
         else if (strstr(filename, ".png") != 0)
             client.println("Content-Type: image/png");
          else if (strstr(filename, ".jpg") != 0)
             client.println("Content-Type: image/jpeg");
         else if (strstr(filename, ".gif") != 0)
             client.println("Content-Type: image/gif");
         else if (strstr(filename, ".3gp") != 0)
             client.println("Content-Type: video/mpeg");
         else if (strstr(filename, ".pdf") != 0)
             client.println("Content-Type: application/pdf");
         else if (strstr(filename, ".js") != 0)
             client.println("Content-Type: application/x-javascript");
         else if (strstr(filename, ".xml") != 0)
             client.println("Content-Type: application/xml");
         else
             client.println("Content-Type: text");
          client.println();
         
  byte cB[1024];
  int cC=0;    
    while (myFile.available()) 
       {
        SPCR &= ~(1<<SPR1); 
        SPCR &= ~(1<<SPR0); 
SPSR |= (1<<SPI2X);
         cB[cC]=myFile.read();
         cC++;
       if(cC > 1023)
        {
        SPCR &= ~(1<<SPR1); 
        SPCR &= ~(1<<SPR0); 
SPSR |= (1<<SPI2X);
         client.write(cB,1024);
        cC=0; 
        } 
       } 
      if(cC > 0) 
      {
        SPCR &= ~(1<<SPR1); 
        SPCR &= ~(1<<SPR0); 
SPSR |= (1<<SPI2X);        
        client.write(cB,cC);    
      }
       myFile.close();
 
        } else {
          // everything else is a 404
          client.println("HTTP/1.1 404 Not Found");
          client.println("Content-Type: text/html");
          client.println();
          client.println("<h2>File Not Found!</h2>");
           }
   break;
      }
    }
    // give the web browser time to receive the data
   delay(1);
    client.stop();
  }
}
}
Конец программы

Недостатки этого Web - сервера
1. Скорость передачи в локальной сети не превышает 20.2-20.4КБайт/с, через Интернет - 17.5-19.1КБайт/с. 
Аналогичный сервер, построенный как указано по ссылке 
в локальной сети у меня работал со скоростью 120-122КБайт/с, в Интернете ~60КБайт/с (для Ethernet контроллера ENC28J60).  
2. Очень упрощенный TCP/IP протокол на W5100. При загрузке канала, например потоком видео с IP камеры не все данные с сервера приходят клиенту.
3. При относительно интенсивном  обращении к серверу происходит обрыв соединения. Это реализовано принудительно в представленной выше программе, иначе микросхема W5100 зависает. 

Пояснения
          USB порт Ардуино необходимо подключить к компьютеру и запустить терминальную программу (например, можно использовать такую программу у Arduino IDE). После подключения к серверу с браузера можно наблюдать распечатку на дисплее. Когда файлы отправляются нормально то:
Packet 0 Packet 1 Packet 0 Packet 1 Packet 0 Packet 1 Packet 0 Packet 1
Если проблемы:
Packet 0 Packet 1 Packet 0 Close Packet 0 Close Packet 0 Close Packet 0 Close Packet 0 Close
Введены изменения в файл socket.cpp:
...
uint16_t send(SOCKET s, const uint8_t * buf, uint16_t len)
{
  uint8_t status=0;
  uint16_t ret=0;
  uint16_t freesize=0;
  if (len > W5100.SSIZE) 
    ret = W5100.SSIZE; // check size not to exceed MAX size.
  else 
    ret = len;
  // if freebuf is available, start.
int timeout=0;
Serial.print(" Packet 0");   // Замирает от сюда
while (freesize < ret){
delay(1);
freesize = W5100.getTXFreeSize(s);
 
if (timeout++ > 1000) {close(s); delay(100);}
status = W5100.readSnSR(s);
if (status == SnSR::CLOSED )
    {
      Serial.print(" Close "); 
      return 0;
    }
}                              // Замирает до сюда
Serial.print(" Packet 1"); 
/*
  do 
  {
    freesize = W5100.getTXFreeSize(s);
    status = W5100.readSnSR(s);
    if ((status != SnSR::ESTABLISHED) && (status != SnSR::CLOSE_WAIT))
    {
      ret = 0; 
      break;
    }
  } 
  while (freesize < ret);
 */
  // copy data
  W5100.send_data_processing(s, (uint8_t *)buf, ret);
  W5100.execCmdSn(s, Sock_SEND);
/*
  while ( (W5100.readSnIR(s) & SnIR::SEND_OK) != SnIR::SEND_OK ) 
  {
    if ( W5100.readSnSR(s) == SnSR::CLOSED )
    {
      close(s);
      return 0;
    }
  }

  W5100.writeSnIR(s, SnIR::SEND_OK); 
*/ 
  return ret;
}
...

Изменены файлы EthernetClient.cpp,  EthernetClient.h
Добавлены функции:
client.sockstat()
client.getlen()
client.disc()
client.clos()
client.sock1()

EthernetClient.cpp:
...
EthernetClient::EthernetClient(uint8_t sock) : _sock(sock) {
}
int EthernetClient::getlen(int s)
{
return W5100.getRXReceivedSize(s);
}
int EthernetClient::sock1()
{
return _sock;
}
void EthernetClient::disc(int i) {
disconnect(i);
}
void EthernetClient::clos(int i) {
close(i);
}
int EthernetClient::sockstat(int s)
{
  return W5100.readSnSR(s) ;
}
...

EthernetClient.h:
...
  virtual void stop();
    virtual int getlen(int s2);
    virtual int sock1();
    virtual int sockstat(int s);
    virtual void disc(int ss);
    virtual void clos(int s1);
  virtual uint8_t connected();
...

Внесены изменение в функцию void EthernetClient::stop()  -  (client.stop()) для файла EthernetClient.cpp:
void EthernetClient::stop() {
  if (_sock == MAX_SOCK_NUM)
    return;

  // attempt to close the connection gracefully (send a FIN to other side)
  disconnect(_sock);
  unsigned long start = millis();
Serial.print(status()); 
  // wait a second for the connection to close
 // while (status() != SnSR::CLOSED && millis() - start < 1000)
  //  delay(1); 
delay(300); 
Serial.print(status()); 
  // if it hasn't closed, close it forcefully
  if (status() != SnSR::CLOSED)
    close(_sock);
Serial.println(status()); 
        Управлять устройствами, подключенными к серверу возможно с помощью программы web - клиента, написанного для Андроид устройства на языке визуального программирования App Inventor 2. Она управляет одним устройством (розеткой), снимает данные с температурного датчика в кнопочном режиме и голосовыми командами, определяет текущее состояние устройства (включено или выключено). 
Программа состоит из двух частей: дизайна с компонентами и набора блоков, реализующих определенные действия. Перед ознакомлением с программой целесообразно посмотреть видео уроки, например, по ссылкам: 
и почитать здесь.

1. Дизайн с компонентами:

2. Блочная программа (1)

3. Блочная программа (2)

4. Блочная программа (3)

5. Блочная программа (4)

Подробное описание работы блоков программы можно посмотреть здесь.

Выводы
1. Можно отметить достаточную устойчивость работы сервера для управления бытовыми устройствами. В течении 2-х лет зависаний не наблюдалось.

Написано 15.11.2015