简体   繁体   中英

Arduino client + PHP cURL server executing command twice

I have this project I'm doing for work in which I command several Arduino (Arduino core + ENC28J60 Ethernet + x4 Relay actuator) modules from a single PHP server to activate a relay on any of the Arduino modules. The server has a list of all the events and executes them as the time is right for each one. What's wrong is that whenever the commands are more than 4 minutes apart (ie >=5 minutes) the command gets executed twice by the Arduino. That is, Arduino activates the relay I commanded twice in a row.

What the code does is this: 1. thor.php is executed lineally once (the task is repeated by a crontab) 2. thor.php searches within its arrays for an occurrence of an event to take place at the current time 3. for each occurrence it generates a task that's delivered to the curl multi handler 4. all the tasks are sent in parallel to each arduino module. 5. when an Arduino receives a request, checks whether it comes from a known IP address and through the allowed port, analyzes the command in the parameters, and activates the relays as requested. 6. the Arduino then sends a response page with a hidden field that will work in the future for control.

In theory all works well, but whenever the commands are 5 minutes or more apart, the Arduino executes the command twice.

I put the whole code next. Here's the Arduino: (Pardon the comments in Spanish)

    #include "etherShield.h"

    //MAC ADDRESS.
    static uint8_t mymac[6] = {
      0x54,0x55,0x58,0x10,0x00,0x24}; 
    //IP ADDRESS THOR.
    static uint8_t myip[4] = {
      172,0,0,101};
    //Unica IP de Origen aceptada.
    static uint8_t ip_origen[4] = {
      172,0,0,10};
    //TCP PORT
    static uint16_t myport = 5566;
    //Setear los pines de los relays. Solo se setea el primero. Se necesitan 4 pines consecutivos libres
    static int primerrelay = 2;

    //Variables globales usadas para el feedbak del modulo en una peticion tcp.
    int16_t comando_rel, comando_tmp;
    //Estado de los relays
    uint8_t estado;

    //Definiciones propias de Arduino. Especifica el tamaño maximo del buffer y lo inicializa.
    #define BUFFER_SIZE 500
    static uint8_t buf[BUFFER_SIZE+1];

    EtherShield es=EtherShield();

    void setup(){

      /*initialize enc28j60*/
      es.ES_enc28j60Init(mymac);
      es.ES_enc28j60clkout(2); // change clkout from 6.25MHz to 12.5MHz
      delay(10);

      /* Magjack leds configuration, see enc28j60 datasheet, page 11 */
      // LEDA=greed LEDB=yellow
      //
      // 0x880 is PHLCON LEDB=on, LEDA=on
      // enc28j60PhyWrite(PHLCON,0b0000 1000 1000 00 00);
      es.ES_enc28j60PhyWrite(PHLCON,0x880);
      delay(500);
      //
      // 0x990 is PHLCON LEDB=off, LEDA=off
      // enc28j60PhyWrite(PHLCON,0b0000 1001 1001 00 00);
      es.ES_enc28j60PhyWrite(PHLCON,0x990);
      delay(500);
      //
      // 0x880 is PHLCON LEDB=on, LEDA=on
      // enc28j60PhyWrite(PHLCON,0b0000 1000 1000 00 00);
      es.ES_enc28j60PhyWrite(PHLCON,0x880);
      delay(500);
      //
      // 0x990 is PHLCON LEDB=off, LEDA=off
      // enc28j60PhyWrite(PHLCON,0b0000 1001 1001 00 00);
      es.ES_enc28j60PhyWrite(PHLCON,0x990);
      delay(500);
      //
      // 0x476 is PHLCON LEDA=links status, LEDB=receive/transmit
      // enc28j60PhyWrite(PHLCON,0b0000 0100 0111 01 10);
      es.ES_enc28j60PhyWrite(PHLCON,0x476);
      delay(100);

      //init the ethernet/ip layer:
      es.ES_init_ip_arp_udp_tcp(mymac,myip,myport);

      //################################
      //Setup de los pines de salida
      for(int i = 0; i < 4; i++)
      {
        pinMode(i + 2, OUTPUT);
      }

      //Lamp-test
      digitalWrite(primerrelay, HIGH);
      delay(100);
      digitalWrite(primerrelay, LOW);
      comando_rel = -1;
      comando_tmp = -1;
    }

    void loop(){
      uint16_t plen, dat_p;

      plen = es.ES_enc28j60PacketReceive(BUFFER_SIZE, buf);

      /*plen will be unequal to zero if there is a valid packet (without crc error) */
      if(plen!=0){

        // arp is broadcast if unknown but a host may also verify the mac address by sending it to a unicast address.
        if(es.ES_eth_type_is_arp_and_my_ip(buf,plen)){
          es.ES_make_arp_answer_from_request(buf);//*******
          return;
        }

        // check if ip packets are for us:
        if(es.ES_eth_type_is_ip_and_my_ip(buf,plen)==0){
          return;
        }

        if(buf[IP_PROTO_P]==IP_PROTO_ICMP_V && buf[ICMP_TYPE_P]==ICMP_TYPE_ECHOREQUEST_V){
          es.ES_make_echo_reply_from_request(buf,plen);
          return;
        }

        // tcp port www start, compare only the lower byte
        // En la siguiente linea esta la clave para poder implementar puertos mayores a 254
        if (buf[IP_PROTO_P]==IP_PROTO_TCP_V&&buf[TCP_DST_PORT_H_P]==highByte(myport)&&buf[TCP_DST_PORT_L_P]==lowByte(myport)){
          if (buf[TCP_FLAGS_P] & TCP_FLAGS_SYN_V){
            es.ES_make_tcp_synack_from_syn(buf); // make_tcp_synack_from_syn does already send the syn,ack
            return;     
          }
          if (buf[TCP_FLAGS_P] & TCP_FLAGS_ACK_V){
            es.ES_init_len_info(buf); // init some data structures
            dat_p=es.ES_get_tcp_data_pointer();
            if (dat_p==0){ // we can possibly have no data, just ack:
              if (buf[TCP_FLAGS_P] & TCP_FLAGS_FIN_V){
                es.ES_make_tcp_ack_from_any(buf);
                //es.ES_make_tcp_ack_from_any(buf, plen, 1);//************
              }
              return;
            }
            //Comparacion de la ip de origen.
            uint8_t match_ip_origen = 1;

            for (int i=0; i<4; i++)
            {
              if(buf[IP_SRC_P + i] != ip_origen[i])
              {
                match_ip_origen = 0;
                break;
              }
            }/**/

            if (match_ip_origen==1)
            {
              if (strncmp("GET ",(char *)&(buf[dat_p]),4)!=0){
                // head, post and other methods for possible status codes see:
                // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
                plen=es.ES_fill_tcp_data_p(buf,0,PSTR("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>200 OK</h1>"));
                goto SENDTCP;
              }
              if (strncmp("/ ",(char *)&(buf[dat_p+4]),2)==0){
                plen=print_webpage(buf);
                goto SENDTCP;
              }

              //Calculo el estado de los pines
              estado = 0; //Se setea en cero antes de hacer la comprobacion
              estado += digitalRead(primerrelay) * 1 + digitalRead(primerrelay + 1) * 2 + digitalRead(primerrelay + 2) * 4 + digitalRead(primerrelay + 3) * 8;

              //#######################################################################
              //Analisis de los parametros y ejecucion de las acciones correspondientes

              if (strncmp("/?cmd=",(char *)&(buf[dat_p+4]),6)==0)
              {
                //cargar los comandos a las variables globales
                analyse_cmd((char *)&(buf[dat_p+10]));
                //Analizar el tiempo. Si es mayor que 0 y menor que 10 (1-9)
                //guardar el estado actual, ejecutar el comando solicitado, y volver al estado anterior.
                //Si el tiempo es positivo menor que 10, setear el estado temporalmente
                if(comando_tmp > 0 && comando_tmp < 10)
                {
                  //Si el valor es aceptable (0-15), se ejecuta el comando
                  if(comando_rel > -1 && comando_rel < 16)
                  {
                    //Generar un estado derivado aplicando un OR a nivel de bits con el estado actual
                    uint8_t r = comando_rel | estado;
                    //Ejecutar el nuevo estado obtenido
                    ejecutar_comando(r);
                    //Esperar el tiempo especificado
                    delay(comando_tmp * 1000);
                    //Volver al estado anterior.
                    ejecutar_comando(estado);
                  }
                }
                //Si el tiempo es igual a cero, setear el nuevo estado indefinidamente
                else if(comando_tmp == 0)
                {
                  //Si el valor es aceptable (0-15), se ejecuta el comando
                  if(comando_rel > -1 && comando_rel < 16)
                  {
                    //Ejecutar el comando y no revertirlo
                    ejecutar_comando(comando_rel);
                  }
                }
              }

              plen=print_webpage(buf);
    SENDTCP:  
              es.ES_make_tcp_ack_from_any(buf); // send ack for http get//***************
              es.ES_make_tcp_ack_with_data(buf,plen); // send data      
            }
          }
        }
      }
    }

    void ejecutar_comando(uint8_t comando)
    {
      //Realiza un and logico con el parametro a nivel de bits. 
      //Enciende o apaga el relay correspondiente.
      //Si el and logico resulta en 0, escribe LOW.
      //Si es diferente a 0, escribe HIGH.
      digitalWrite(primerrelay, (comando & 1));
      digitalWrite(primerrelay + 1, (comando & 2)); 
      digitalWrite(primerrelay + 2, (comando & 4)); 
      digitalWrite(primerrelay + 3, (comando & 8));
    }

    void analyse_cmd(char *x)
    {
      //por por default si no hubieran llegado comandos o estan mal 
      comando_rel = -1;
      comando_tmp = -1;
      //verificar que esten todos los caracteres requeridos
      uint8_t i = 0;
      while(x[i]!=' ' && x[i]!='\0' && i < 10){
        i++;
      }
      //si tiene 4 son los caracteres necesarios: 2 para los reles y 2 para el timer
      if(i==4){
        String aux = "";
        //verificar por el nro de los reles
        if(is_integer(x[0]) && is_integer(x[1])){
          aux = String(x[0]) + String(x[1]);
          comando_rel = aux.toInt();
        }
        aux = "";
        //verificar por el nro de segundos del timer
        if(is_integer(x[2]) && is_integer(x[3])){
          aux = String(x[2]) + String(x[3]);
          comando_tmp = aux.toInt();
        }          
      }
    }

    uint8_t is_integer(char c){
      uint8_t r = 0;
      if (c < 0x3a && c > 0x2f){
        r = 1;
      }  
      return r;
    }

    uint16_t print_webpage(uint8_t *buf)
    {
      uint16_t plen, dat_p;
      dat_p=es.ES_get_tcp_data_pointer();

      plen=es.ES_fill_tcp_data_p(buf,0,PSTR("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n"));

      plen=es.ES_fill_tcp_data_p(buf,plen,PSTR("<center><p><h1>Modulo Thor V1.0  </h1></p></br></hr> "));

      String x = String(buf[IP_DST_P]) + "." + String(buf[IP_DST_P+1]) + "." + String(buf[IP_DST_P+2]) + "." + String(buf[IP_DST_P+3]) + " llamado desde ";
      char *s = getCharArray(x);
      plen=es.ES_fill_tcp_data(buf,plen,s);

      x = String(buf[IP_SRC_P]) + "." + String(buf[IP_SRC_P+1]) + "." + String(buf[IP_SRC_P+2]) + "." + String(buf[IP_SRC_P+3]) + "</br></center>";
      s = getCharArray(x);
      plen=es.ES_fill_tcp_data(buf,plen,s);

      //Al haberse ejecutado un comando el estado resultante debe ser actualizado.
      //Calculo del estado de los pines
      estado = 0; //Se setea en cero antes de hacer la comprobacion
      estado += digitalRead(primerrelay) * 1 + digitalRead(primerrelay + 1) * 2 + digitalRead(primerrelay + 2) * 4 + digitalRead(primerrelay + 3) * 8;

      x = "REL: " + String(comando_rel) + "</br>TMP: " + String(comando_tmp) + "</br>STA: " + String(estado) + "</br></br>"
+ "<input type=\"hidden\" name=\"status\" value=\"" + (String)estado + "\">";
      s = getCharArray(x);
      plen=es.ES_fill_tcp_data(buf,plen,s);

      return(plen);
    }

    char* getCharArray(String s)
    {
      char charBuf[s.length() + 1];
      s.toCharArray(charBuf,s.length() + 1);
      return charBuf;
    }

    void reset()
    {
      for (int i = primerrelay; i < primerrelay + 4; i++)
      {
        digitalWrite(i, LOW);
      }
    }

thor.php:

    <?php
    //Requiere tener instalado php5-curl
    require 'thorconfig.php';
    require 'common.php';
    $tareas = array();
    //Recorrer las configuraciones y armar la lista de tareas
            foreach($modulos as $modulo) //Recorrer cada módulo
            {
        foreach($modulo["eventos"] as $evento) //Recorrer cada evento de un módulo
        {
            //Si el día y la hora del evento coinciden con el día y la hora actuales
            if(strcmp(date("w"), $evento["dia"]) == 0 && strcmp(date("H:i"), $evento["hora"]) == 0)
            {
                //Añadir una tarea con el formato "http://direccion_ip:puerto/?cmd=reltmp"
                $tareas[] = "http://".$modulo["ip"].":".$modulo["puerto"]."/?cmd=".$evento["rel"].$evento["tmp"];
            }
        }
    }
    $curl = array();
    //Inicializar el handler de tareas
    $curlHandle = curl_multi_init();
    //Recorrer las tareas y añadirlas al handler
    foreach($tareas as $tarea)
        $curl[] = addHandle($curlHandle, $tarea);
    //Ejecutar el handler
    ExecHandle($curlHandle);
    echo "\n";
    //Recuperar la respuesta de cada tarea ejecutada
    for($i = 0; $i < sizeof($tareas); $i++)
    {
        $respuesta = curl_multi_getcontent($curl[$i])."\n";
        if(!strpos($respuesta, "<input type=\"hidden\" name=\"status\""))
        {
            $message = "Ha ocurrido un error al intentar ejecutar el siguiente comando: ".$tareas[$i];
            sendMail($server["from"], $server["from"], $server["to"], $server["to"], "Error en Thor", $message, $server);
        }
        else
        {
            echo $respuesta;
        }
    }
    //Remover cada tarea del handler
    foreach($curl as $handle)
        curl_multi_remove_handle($curlHandle, $handle);
    //Cerrar el handler
    curl_multi_close($curlHandle);
    ?>

thorconfig.php

    <?php
        $modulos = [
            "modulo 0" => [
                "ip" => "172.24.51.101", //Teológico
                "puerto" => 6174,
                "eventos" => [
                    ////////////////////// Lunes //////////////////////
                    "evento 0" => [
                        "dia" => 1,
                        "hora" => "07:30",
                        "rel" => "01",
                        "tmp" => "03"
                    ],
                    "evento 1" => [
                        "dia" => 1,
                        "hora" => "08:25",
                        "rel" => "01",
                        "tmp" => "03"
                    ]
    .
    .
    .

           ]
    ]

        $server = [
            "host" => "172.16.0.40",
            "puerto" => 25,
            "smtpuser" => "user",
            "smtppass" => "pass",
            "to" => "mail@uap.edu.ar",
            "from" => "mail@uap.edu.ar"
        ];
    ?>

common.php:

    <?php
    //Función que ejecuta el handler
    function ExecHandle(&$curlHandle)
    {
            $flag=null;
            do {
            //fetch pages in parallel
                    curl_multi_exec($curlHandle,$flag);
            } while ($flag > 0);
    }

    //Función que añade un recurso al handler
    function addHandle(&$curlHandle,$url)
    {
            $cURL = curl_init();
            curl_setopt($cURL, CURLOPT_URL, $url);
            curl_setopt($cURL, CURLOPT_HEADER, 0);
            curl_setopt($cURL, CURLOPT_RETURNTRANSFER, 1);
            curl_multi_add_handle($curlHandle,$cURL);
            return $cURL;
    }

    function sendMail($from, $namefrom, $to, $nameto, $subject, $message, $server)
    {
            $smtpServer = $server["host"];   //ip address of the mail server.  This can also be the local domain name
            $port = $server["puerto"];                    // should be 25 by default, but needs to be whichever port the mail server will be using for smtp
            $timeout = "45";                 // typical timeout. try 45 for slow servers
            $username = $server["smtpuser"]; // the login for your smtp
            $password = $server["smtppass"];           // the password for your smtp
            $localhost = "127.0.0.1";      // Defined for the web server.  Since this is where we are gathering the details for the email
            $newLine = "\r\n";           // aka, carrage return line feed. var just for newlines in MS
            $secure = 0;                  // change to 1 if your server is running under SSL

            //connect to the host and port
            $smtpConnect = fsockopen($smtpServer, $port, $errno, $errstr, $timeout);
            $smtpResponse = fgets($smtpConnect, 4096);
            if(empty($smtpConnect)) {
                     $output = "Failed to connect: $smtpResponse";
                     echo $output;
                     return $output;
            }
            else {
                     $logArray['connection'] = "<p>Connected to: $smtpResponse";
                     echo "<p />connection accepted<br>".$smtpResponse."<p />Continuing<p />\n";
            }

            //you have to say HELO again after TLS is started
            fputs($smtpConnect, "HELO $localhost". $newLine);
            $smtpResponse = fgets($smtpConnect, 4096);
            $logArray['heloresponse2'] = "$smtpResponse";
            //request for auth login
            fputs($smtpConnect,"AUTH LOGIN" . $newLine);
            $smtpResponse = fgets($smtpConnect, 4096);
            $logArray['authrequest'] = "$smtpResponse";

            //send the username
            fputs($smtpConnect, base64_encode($username) . $newLine);
            $smtpResponse = fgets($smtpConnect, 4096);
            $logArray['authusername'] = "$smtpResponse";

            //send the password
            fputs($smtpConnect, base64_encode($password) . $newLine);
            $smtpResponse = fgets($smtpConnect, 4096);
            $logArray['authpassword'] = "$smtpResponse";

            //email from
            fputs($smtpConnect, "MAIL FROM: <$from>" . $newLine);
            $smtpResponse = fgets($smtpConnect, 4096);
            $logArray['mailfromresponse'] = "$smtpResponse";

            //email to
            fputs($smtpConnect, "RCPT TO: <$to>" . $newLine);
            $smtpResponse = fgets($smtpConnect, 4096);
            $logArray['mailtoresponse'] = "$smtpResponse";

            //the email
            fputs($smtpConnect, "DATA" . $newLine);
            $smtpResponse = fgets($smtpConnect, 4096);
            $logArray['data1response'] = "$smtpResponse";

            //construct headers
            $headers = "MIME-Version: 1.0" . $newLine;
            $headers .= "Content-type: text/html; charset=iso-8859-1" . $newLine;
            $headers .= "To: $nameto <$to>" . $newLine;
            $headers .= "From: $namefrom <$from>" . $newLine;

            //observe the . after the newline, it signals the end of message
            fputs($smtpConnect, "To: $to\r\nFrom: $from\r\nSubject: $subject\r\n$headers\r\n\r\n$message\r\n.\r\n");
            $smtpResponse = fgets($smtpConnect, 4096);
            $logArray['data2response'] = "$smtpResponse";

            // say goodbye
            fputs($smtpConnect,"QUIT" . $newLine);
            $smtpResponse = fgets($smtpConnect, 4096);
            $logArray['quitresponse'] = "$smtpResponse";
            $logArray['quitcode'] = substr($smtpResponse,0,3);
            fclose($smtpConnect);
            //a return value of 221 in $retVal["quitcode"] is a success
            return($logArray);  
    }
    ?>

Any ideas why it executes only once, as it should, when I execute commands within less than 4 minutes and it executes twice otherwise?

EDIT: I discarded the problem being in the PHP code. I installed a lynx text browser in the server and manually executed commands more than 5 minutes apart and got the same result: a duplicated action from Arduino. I leave the PHP code just in case someone is interested in it and may use it. I'll continue to experiment to find a solution.

EDIT 2: I discarded the problem being in the Arduino Hardware. I tested an out-of-the-box new Arduino Uno (same model) with the same code and it still had the same bug.

EDIT 3: Just an idea. Is it possible that the PHP server is expecting an immediate response and as it isn't given by Arduino right away then sends the packet again thus getting a double (late) response from Arduino? Here's another one: Is it possible that the Arduino is passing through the buffer twice and not realizing it? (The second options seems less likely to me).

Use a network sniffer on your server (ie wireshark) to see what realy gets send. That way you can easily test for your idea no 3. wireshark might also have an option to replay traffic, thus making testing easier.

I "fixed" the problem by adding a Patch (Not a permanent solution). I still need to find the root of the problem.

Added this after the #include "etherShield.h" line

#include <TimedAction.h>

//Para control de ejecuciones
int16_t ultimo_comando_ejecutado[5];
int16_t ultimo_tiempo_ejecutado[5];
//Number of seconds since last command execution or execution register.
int8_t segundos;
TimedAction ta = TimedAction(1000,revisar);

this in the setup()

resetear_registro();
segundos = 0;

this in the first line of loop()

ta.check();

this on command execution

if(ejecutado(comando_rel, comando_tmp) == 0)
{
  ejecutar_comando(r);
  registrar_ejecucion(comando_rel, comando_tmp);
  //Esperar el tiempo especificado
  delay(comando_tmp * 1000);
  //Volver al estado anterior.
  ejecutar_comando(estado);
}

and this

if(ejecutado(comando_rel, comando_tmp) == 0)
{
  ejecutar_comando(comando_rel);
  registrar_ejecucion(comando_rel, comando_tmp);
}

Finally, these functions at the end

void registrar_ejecucion(int16_t cmd, int16_t tmp)
{
  for(int i = 0; i < 4; i++)
  {
    ultimo_comando_ejecutado[i] = ultimo_comando_ejecutado[i+1];
    ultimo_tiempo_ejecutado[i] = ultimo_tiempo_ejecutado[i+1];
  }
  ultimo_comando_ejecutado[4] = cmd;
  ultimo_tiempo_ejecutado[4] = tmp;
  segundos = 0;
}

uint8_t ejecutado(int16_t cmd, int16_t tmp)
{
  uint8_t ejec = 0;
  for(int i = 0; i < 5; i++)
  {
    if(ultimo_comando_ejecutado[i] == cmd && ultimo_tiempo_ejecutado[i] == tmp)
    {
      ejec = 1;
      break;
    }
  }
  return ejec;
}

void resetear_registro()
{
  for(int a = 0; a < 5; a++)
  {
    ultimo_comando_ejecutado[a] = -1;
    ultimo_tiempo_ejecutado[a] = -1;
  }
}

void revisar()
{
  segundos++;
  if(segundos > 59)
  {
    resetear_registro();
    segundos = 0;
  }
}

Basically what it does is check the last 5 commands (relays AND time) executed within the last minute and if there's a match, the command is ignored. On the low scale it solves the problem. But I am conscious that it is only a patch and that problems may arise. For now I'm going to implement the code as is (plus patch). But if anyone finds a better and more permanent solution, I'm open to suggestions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM