2

我有这个项目正在做的工作,我从单个 PHP 服务器命令几个 Arduino(Arduino 核心 + ENC28J60 以太网 + x4 继电器执行器)模块来激活任何 Arduino 模块上的继电器。服务器有一个所有事件的列表,并在每个事件合适的时候执行它们。问题在于,只要命令相隔超过 4 分钟(即 >=5 分钟),Arduino 就会执行两次命令。也就是说,Arduino 激活了我连续两次命令的继电器。

代码的作用是: 1. thor.php 线性执行一次(任务由 crontab 重复) 2. thor.php 在其数组中搜索当前时间发生的事件 3. 对于每次出现它都会生成一个任务,该任务被传递给 curl 多处理程序 4。所有任务都并行发送到每个 arduino 模块。5. Arduino收到请求时,检查是否来自已知IP地址和允许的端口,分析参数中的命令,并根据请求激活继电器。6. Arduino 然后发送一个带有隐藏字段的响应页面,该隐藏字段将在未来用于控制。

理论上一切正常,但只要命令相隔 5 分钟或更长时间,Arduino 就会执行两次命令。

我把整个代码放在后面。这是Arduino:(请原谅西班牙语的评论)

    #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);
      }
    }

雷神.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"
        ];
    ?>

常见的.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);  
    }
    ?>

当我在不到 4 分钟的时间内执行命令时它应该只执行一次,否则它会执行两次,有什么想法吗?

编辑:我放弃了 PHP 代码中的问题。我在服务器中安装了一个 lynx 文本浏览器,并在 5 分钟内手动执行命令,得到了相同的结果:来自 Arduino 的重复操作。我留下 PHP 代码以防万一有人对它感兴趣并可能使用它。我将继续尝试以找到解决方案。

编辑 2:我放弃了 Arduino 硬件中的问题。我用相同的代码测试了一个开箱即用的新 Arduino Uno(相同型号),它仍然有相同的错误。

编辑3:只是一个想法。PHP服务器是否有可能期望立即响应并且由于Arduino没有立即给出它然后再次发送数据包从而从Arduino获得双重(延迟)响应?这是另一个问题:Arduino 是否有可能两次通过缓冲区而没有意识到?(对我来说,第二种选择似乎不太可能)。

4

2 回答 2

0

我通过添加补丁“修复”了这个问题(不是永久解决方案)。我仍然需要找到问题的根源。

#include "etherShield.h" 在行后添加了这个

#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);

这在setup()

resetear_registro();
segundos = 0;

这在第一行loop()

ta.check();

这个关于命令执行

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);
}

和这个

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

最后把这些函数放在最后

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;
  }
}

基本上它所做的是检查最后一分钟内执行的最后 5 个命令(继电器和时间),如果匹配,则忽略该命令。在小范围内,它解决了这个问题。但我意识到这只是一个补丁,可能会出现问题。现在我将按原样实现代码(加上补丁)。但如果有人找到更好、更持久的解决方案,我愿意接受建议。

于 2013-03-28T14:15:12.167 回答
0

在您的服务器上使用网络嗅探器(即wireshark)来查看真正发送的内容。这样您就可以轻松地测试您的想法 3。wireshark 还可以选择重放流量,从而使测试更容易。

于 2013-03-27T13:09:14.970 回答