Клиент Arduino + сервер PHP cURL, выполняющий команду дважды
У меня есть этот проект, который я выполняю для работы, в которой я управляю несколькими модулями Arduino (Arduino core + ENC28J60 Ethernet + x4 Relay) с одного PHP-сервера, чтобы активировать реле на любом из модулей Arduino. Сервер имеет список всех событий и выполняет их, так как время подходит для каждого. Неправильно то, что всякий раз, когда команды находятся на расстоянии более 4 минут (т. Е. >=5 минут), команда исполняется дважды Arduino. То есть Arduino активирует реле, которым я командовал два раза подряд.
Код выполняет следующие действия: 1. thor.php выполняется линейно один раз (задание повторяется crontab) 2. thor.php ищет в своих массивах событие, которое должно произойти в текущий момент времени 3. для в каждом случае он генерирует задачу, которая доставляется в обработчик curl multi 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);
}
}
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);
}
?>
Любые идеи, почему он выполняется только один раз, как это должно быть, когда я выполняю команды менее чем за 4 минуты и в противном случае он выполняется дважды?
РЕДАКТИРОВАТЬ: я исключил проблему в коде PHP. Я установил на сервере текстовый браузер lynx и вручную выполнил команды с интервалом более 5 минут и получил тот же результат: дублированное действие от Arduino. Я оставляю код PHP на тот случай, если кто-то заинтересован в нем и может его использовать. Я продолжу экспериментировать, чтобы найти решение.
РЕДАКТИРОВАТЬ 2: я отбросил проблему, будучи в Arduino Hardware. Я протестировал новый Arduino Uno из той же модели, что и из коробки, с тем же кодом, и он все еще имел ту же ошибку.
РЕДАКТИРОВАТЬ 3: Просто идея. Возможно ли, что сервер PHP ожидает немедленного ответа и, поскольку он не передается Arduino сразу, затем отправляет пакет снова, получая двойной (поздний) ответ от Arduino? Вот еще один: возможно ли, что Arduino дважды проходит через буфер и не осознает этого? (Второй вариант мне кажется менее вероятным).
2 ответа
Используйте сетевой сниффер на вашем сервере (например, wireshark), чтобы увидеть, что действительно получает отправка. Таким образом, вы можете легко протестировать свою идею № 3. У wireshark также может быть возможность воспроизвести трафик, что упростит тестирование.
Я "исправил" проблему, добавив патч (не постоянное решение). Мне все еще нужно найти корень проблемы.
Добавил это после#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 команд (реле и время), выполненных за последнюю минуту, и, если есть совпадение, команда игнорируется. В низком масштабе это решает проблему. Но я осознаю, что это всего лишь патч и проблемы могут возникнуть. Сейчас я собираюсь реализовать код как есть (плюс патч). Но если кто-то найдет лучшее и более постоянное решение, я открыт для предложений.