我有这个项目正在做的工作,我从单个 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 是否有可能两次通过缓冲区而没有意识到?(对我来说,第二种选择似乎不太可能)。