2013-03-25 171 views
2

我有這個項目,我正在做的工作中,我從一個PHP服務器命令幾個Arduino(Arduino核心+ ENC28J60以太網+ x4繼電器執行器)模塊來激活一個繼電器任何Arduino模塊。服務器有一個所有事件的列表並執行它們,因爲每個事件的時間都是正確的。什麼是錯誤的是,只要命令間隔超過4分鐘(即> = 5分鐘),該命令將由Arduino執行兩次。也就是說,Arduino激活了我連續兩次指揮的繼電器。Arduino客戶端+ PHP cURL服務器執行命令兩次

什麼代碼的作用是這樣的: 1. thor.php執行直線地一次(該任務是通過一個crontab重複) 2. thor.php搜索其陣列內的一個事件的發生,以發生在每次出現當前的時間 3.它生成投放給捲曲多處理器 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" => "[email protected]", 
      "from" => "[email protected]" 
     ]; 
    ?> 

的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硬件中的問題。我用相同的代碼測試了一個開箱即用的新Arduino Uno(同一型號),它仍然有相同的錯誤。

編輯3:只是一個想法。是否有可能的是,PHP服務器要求立即作出反應,當它不被賦予的Arduino馬上然後重新發送數據包從而獲得從Arduino的雙(已故)的反應?這裏有另一個:是否有可能Arduino兩次通過緩衝區而沒有意識到? (第二種選擇似乎不太可能)。

+0

這是關於一個月你問這裏的問題,看起來很具體。你對它有什麼牽引力? – hakre 2013-04-26 14:38:29

回答

0

在您的服務器上使用網絡嗅探器(即wireshark)查看真正發送的內容。這樣,您就可以輕鬆測試您的想法。3. wireshark也可以選擇重播流量,從而使測試更輕鬆。

0

我通過添加補丁(不是永久解決方案)「解決了」問題。我仍然需要找到問題的根源。

將此添加在loop()

ta.check(); 

此命令上的exe第一行中的 #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; 

此之後cution

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個命令中的最後執行(繼電器和時間)分鐘,如果匹配,該命令將被忽略。 在低規模上解決了這個問題。但我意識到這只是一個補丁,可能會出現問題。 現在我要按原樣執行代碼(加上補丁)。但是如果有人找到更好更持久的解決方案,我願意接受建議。

相關問題