Continuando la seguidilla de entradas con Arduino, vamos a comentar como se fue dando el desarrollo de esta básica interfaz web para poder administrar los parámetros de red del Ethernet Shield W5100 conectado a nuestro Arduino. La idea era poder enviar los parámetros por POST, usar Basic Authentication para el acceso a la sección de configuración y poder restaurar los valores a un estado original con un botón de reset.

Librerías utilizadas

Para resolver el servicio web, el proyecto Webduino tiene muy desarrollada la librería WebServer.h (disponible aquí) que nos simplifica el intercambio de datos vía POST y resolver el acceso con Basic Authentication similar al del .htaccess de Apache.
Para poder almacenar los datos en memoria utilizamos la librería EEPROM.h (disponible aquí) que implementa los métodos write y read para escribir y leer de manera sencilla sobre la memoria del Arduino.
SoftReset.h (disponible aquí) es una implementación que permite hacer un reseteo de la placa Arduino desde el mismo software.
Por último incorporamos rBase64.h (disponible aquí) que nos permite codificar y decodificar a Base64.

Basic Access Authentication

Es un método que permite que un agente de usuario se autentique enviando en la cabecera HTTP usuario:contraseña en Base64.
Por ejemplo para usuario admin y contraseña admin, si codificamos admin:admin en base64 obtenemos YWRtaW46YWRtaW4= . Para verificar esto podríamos utilizar el servicio web de https://www.base64encode.org/.

Authorization: Basic YWRtaW46YWRtaW4=

Si observamos las siguientes lineas del código, el método checkCredentials verifica si los parámetros enviados por el cliente son iguales al usuario:contraseña almacenados en la variable cUserPass64. Si no coinciden se envía un HTTP 401 Unauthorized status.

char cUserPass64[45] = "YWRtaW46YWRtaW4=";              // admin:admin en base64
.
.
.
if (client.checkCredentials(cUserPass64)){   
    client.httpSuccess();
}else{
    client.httpUnauthorized();
}

Recibiendo los parámetros del formulario por POST

El método readPostparam() de la clase WebServer dentro de un loop nos permite recibir todos los POST REQUEST y poder asignarlos a una variable del programa, como por ejemplo ip_dns[].

if (type == WebServer::POST){
    bool repeat;
    char name[16];
    char value[16];
 
    do{
      repeat = client.readPOSTparam(name, 16, value, 16);
    .
    .
    .     
       ip_dns[(int)name[1] - 48]=strtoul(value, NULL, 10);
    .
    .
    .
    } while (repeat);

Para el caso de un cambio de contraseña del usuario admin, debemos concatenar anteponiendo admin: a la nueva contraseña y codificarla en Base64. Esto lo podemos ver en las siguientes líneas del programa.

strcpy(cUserPass, "admin:");
strcat(cUserPass, value);
rbase64.encode(cUserPass).toCharArray(cUserPass64,45);

Escribiendo la memoria del Arduino
Los métodos write y read de la clase EEPROM nos permiten acceder directamente a una posición de memoria como se observa en las siguientes lineas de nuestro programa donde escribimos la IP entre las posiciones de memoria 7 y 10.

EEPROM.write(7, ip[0]);
EEPROM.write(8, ip[1]);
EEPROM.write(9, ip[2]);
EEPROM.write(10, ip[3]);

De manera similar, accedemos para su lectura y asignación a la variable ip[].

ip[0] = EEPROM.read(7);
ip[1] = EEPROM.read(8);
ip[2] = EEPROM.read(9);
ip[3] = EEPROM.read(10);

Nosotros definimos un byte definido en la constante ID_KNOW y almacenada en la primer posición de memoria que nos indica si la misma ya fue escria en alguna iteración o se toman los valores de red definidos por defecto. Esto se puede verificar en la sección ShieldSetup.

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
byte ip[] = {192,168,1,2};
byte subnet[] = {255,255,0,0};
byte ip_gateway[] = {192,168,1,1};
byte ip_dns[] = {192,168,1,1};
char cUserPass[] = "admin:admin";
char cUserPass64[45] = "YWRtaW46YWRtaW4=";              // admin:admin en base64
 
const byte ID_KNOW = 0x92;                              // Usado para identificar si se grabaron datos en la EEPROM 
char buffer[100];
void ShieldSetup(){
  //  Serial.begin(9600);
  int idcheck = EEPROM.read(0);
 
  if (idcheck == ID_KNOW){
    mac[0] = EEPROM.read(1); mac[1] = EEPROM.read(2); mac[2] = EEPROM.read(3); mac[3] = EEPROM.read(4); mac[4] = EEPROM.read(5); mac[5] = EEPROM.read(6); 
    ip[0] = EEPROM.read(7); ip[1] = EEPROM.read(8); ip[2] = EEPROM.read(9); ip[3] = EEPROM.read(10); 
    subnet[0] = EEPROM.read(11); subnet[1] = EEPROM.read(12); subnet[2] = EEPROM.read(13); subnet[3] = EEPROM.read(14); 
    ip_gateway[0] = EEPROM.read(15); ip_gateway[1] = EEPROM.read(16); ip_gateway[2] = EEPROM.read(17); ip_gateway[3] = EEPROM.read(18); 
    ip_dns[0] = EEPROM.read(19); ip_dns[1] = EEPROM.read(20); ip_dns[2] = EEPROM.read(21); ip_dns[3] = EEPROM.read(22); 
    for (int i=0;i<43;i++){
      cUserPass64[i] = EEPROM.read(i+23); 
    }
  }
  Ethernet.begin(mac, ip, ip_dns, ip_gateway, subnet);
}

La interfaz web

Todo esto nos brinda un entorno para la administración de los parámetros de red protegido con usuario y contraseña pero nos restaba poder volver a los valores por defecto en el caso de perder la contraseña de administrador o el acceso de red. Imitando el comportamiento de los routers hogareños se nos ocurrió utilizar una de las entradas/salidas del Arduino para agregarle un botón de reset que se comporte como reinicio del dispositivo si se oprime por un instante, o reinicio a los valores establecidos por default si se mantiene por mas de 3 segundos.

Para ello fue necesario agregar un pulsador con una resistencia de 4,7k a 10k en Pull Down (ver el siguiente esquema).

El cableado

Las resistencias de Pull-Down o Pull-Up se conectan entre el PIN digital y una de las tensiones de referencia (0V o 5V) y “fuerzan” (de ahí su nombre) el valor de la tensión a LOW o HIGH, respectivamente.

La resistencia de Pull-Up fuerza HIGH cuando el pulsador está abierto. Cuando está cerrado el PIN se pone a LOW, la intensidad que circula se ve limitada por esta resistencia
La resistencia de Pull-Down fuerza LOW cuando el pulsador está abierto. Cuando está cerrado el PIN se pone a HIGH, y la intensidad que circula se ve limitada por esta resistencia.

El código de reset

Definiendo algunas constantes y variables en el programa, solo debemos monitorear dentro del loop() las veces que el switch conectado a nuestra entrada es pulsado y por cuanto tiempo. Para ello tenemos que registrar los cambios de estado y los milisegundos que tomó este paso, mediante la función millis(). Dependiendo el tiempo oprimido, solo debemos modificar el primer byte de la memoria a un estado diferente al de ID_KNOW y resetear mediante la función soft_restart() el Arduino.

const int switchPin = ;                 // Usado para identifiar la entrada digital al Switch de Reset de Fabrica
int valueReset = 0;                     // Cablear resistencia 4,7k entre Gnd y entrada digital (switchPin)
int prevValueReset = 0;                 // El Switch se cablea entre 5V y la entrada digital (switchPin)
uint32_t prevMillisReset = millis();
 
void setup(){
  pinMode(switchPin, INPUT);
}
 
void loop(){
  prevValueReset = valueReset;
  valueReset = digitalRead(switchPin);
 
  if (valueReset == HIGH && prevValueReset == LOW) {                // Si oprimo el boton
    prevMillisReset = millis(); 
  }else if (valueReset == LOW && prevValueReset == HIGH) {          // Si suelto el boton 
    if(millis()-prevMillisReset>3000){                              // Si es mayor a 3 segundos Reset a Fabrica
      EEPROM.write(0, 0x00);   
    }                                                               // Reseteo el equipo
    soft_restart();
  }
}

El código completo se puede observar en las siguientes lineas donde restaría implementar los métodos de validación de los parámetros de red.

/*************************************************************************
  Configuracion de red via web con usuario/password y boton de reset
  Version: 0.5
  Fecha: 27/02/2018
  Mas informacion: https://pablo.sarubbi.com.ar
*************************************************************************/
#define WEBDUINO_FAVICON_DATA ""                        // Resta 198 byte al codigo por el Favicon del Webserver
#include <WebServer.h>                                  // https://github.com/sirleech/Webduino
#include <EEPROM.h>                                     // https://www.arduino.cc/en/Reference/EEPROM
#include <SoftReset.h>                                  // https://github.com/WickedDevice/SoftReset
#include <rBase64.h>                                    // https://github.com/boseji/rBASE64
 
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
byte ip[] = {192,168,1,2};
byte subnet[] = {255,255,0,0};
byte ip_gateway[] = {192,168,1,1};
byte ip_dns[] = {192,168,1,1};
char cUserPass[] = "admin:admin";
char cUserPass64[45] = "YWRtaW46YWRtaW4=";              // admin:admin en base64
 
const byte ID_KNOW = 0x92;                              // Usado para identificar si se grabaron datos en la EEPROM 
char buffer[100];
 
const int switchPin = 2;                                // Usado para identifiar la entrada digital al Switch de Reset de Fabrica
int valueReset = 0;                                     // Cablear resistencia 4,7k entre Gnd y entrada digital (switchPin)
int prevValueReset = 0;                                 // El Switch se cablea entre 5V y la entrada digital (switchPin)
uint32_t prevMillisReset = millis();
 
#define PREFIX ""
WebServer webserver(PREFIX, 80);
 
void defaultCmd(WebServer &client, WebServer::ConnectionType type, char *, bool){
  client.httpSuccess();
  if (type != WebServer::HEAD){
    client.print(F("Hola Mundo!"));
  }
}
 
void privateCmd(WebServer &client, WebServer::ConnectionType type, char *, bool){
 
  if (client.checkCredentials(cUserPass64)){          
    client.httpSuccess();
    if (type != WebServer::HEAD){
      if (type == WebServer::POST){
        bool repeat;
        char name[16];
        char value[16];
 
        do{
          repeat = client.readPOSTparam(name, 16, value, 16);
          switch (name[0]){
          case 'm':                                                 // Si es la mac address
            mac[(int)name[1] - 48]=strtoul(value, NULL, HEX);
            break;
          case 'i':                                                 // Si es la IP
            ip[(int)name[1] - 48]=strtoul(value, NULL, 10);
            break;
          case 's':                                                 // Si es la mascara de subred
            subnet[(int)name[1] - 48]=strtoul(value, NULL, 10);
            break;
          case 'g':                                                 // Si es el GATEWAY
            ip_gateway[(int)name[1] - 48]=strtoul(value, NULL, 10);
            break;
          case 'd':                                                 // Si es el DNS
            break; 
          case 'p':
            strcpy(cUserPass, "admin:");
            strcat(cUserPass, value);
            rbase64.encode(cUserPass).toCharArray(cUserPass64,45);
            break;  
          }
        } while (repeat);
 
        // Guardo los datos en la EEPROM
        EEPROM.write(0, ID_KNOW); 
        EEPROM.write(1, mac[0]); EEPROM.write(2, mac[1]); EEPROM.write(3, mac[2]); EEPROM.write(4, mac[3]); EEPROM.write(5, mac[4]); EEPROM.write(6, mac[5]); 
        EEPROM.write(7, ip[0]); EEPROM.write(8, ip[1]); EEPROM.write(9, ip[2]); EEPROM.write(10, ip[3]); 
        EEPROM.write(11, subnet[0]); EEPROM.write(12, subnet[1]); EEPROM.write(13, subnet[2]); EEPROM.write(14, subnet[3]); 
        EEPROM.write(15, ip_gateway[0]); EEPROM.write(16, ip_gateway[1]); EEPROM.write(17, ip_gateway[2]); EEPROM.write(18, ip_gateway[3]); 
        EEPROM.write(19, ip_dns[0]); EEPROM.write(20, ip_dns[1]); EEPROM.write(21, ip_dns[2]); EEPROM.write(22, ip_dns[3]);
        // Grabo el BASE64 de admin:password entre las posiciones de memoria 23 y 65
        for (int i=0; i < 43; i++){
          EEPROM.write(i+23, String(cUserPass64)[i]);
        }
 
        client.print("<meta http-equiv=\"refresh\" content=\"3; url=http://");
        client.print(ip[0],DEC);
        for (int i= 1; i < 4; i++){
          client.print(".");
          client.print(ip[i],DEC);
        }
        client.println("/index.html\" >");
        client.reset();
        delay(1);
        soft_restart();
      } else {
        client.println(F("<html><title>Arduino Ethernet WebSetup</title>"));
        client.println(F("<body marginwidth=\"0\" marginheight=\"0\" leftmargin=\"0\" style=\"margin: 0; padding: 0;\">"));
        client.println(F("<script>function hex2num (s_hex) {eval(\"var n_num=0X\" + s_hex);return n_num;}</script>"));
        client.println(F("<form method='post'>"));
        client.println(F("<table  border=\"0\" width=\"100%\" cellpadding=\"1\" style=\"font-family:Verdana;font-size:12px;\">"));
 
        client.println(F("\t<tr><td colspan=\"2\" style=\"background-color: #999999;\">Ethernet</td></tr>"));
 
        client.println(F("\t<tr><td>MAC:</td><td>"));
        client.println(F("\t\t<input type=\"hidden\" name=\"SBM\" value=\"1\">"));
        client.print(F("\t\t<input id=\"m0\" type=\"text\" size=\"2\" maxlength=\"2\" name=\"m0\" value=\"")); client.print(mac[0],HEX); client.println(F("\">:"));
        client.print(F("\t\t<input id=\"m1\" type=\"text\" size=\"2\" maxlength=\"2\" name=\"m1\" value=\"")); client.print(mac[1],HEX); client.println(F("\">:"));
        client.print(F("\t\t<input id=\"m2\" type=\"text\" size=\"2\" maxlength=\"2\" name=\"m2\" value=\"")); client.print(mac[2],HEX); client.println(F("\">:"));
        client.print(F("\t\t<input id=\"m3\" type=\"text\" size=\"2\" maxlength=\"2\" name=\"m3\" value=\"")); client.print(mac[3],HEX); client.println(F("\">:"));
        client.print(F("\t\t<input id=\"m4\" type=\"text\" size=\"2\" maxlength=\"2\" name=\"m4\" value=\"")); client.print(mac[4],HEX); client.println(F("\">:"));
        client.print(F("\t\t<input id=\"m5\" type=\"text\" size=\"2\" maxlength=\"2\" name=\"m5\" value=\"")); client.print(mac[5],HEX); client.println(F("\">"));
        client.println(F("\t</td></tr>"));
 
        client.println(F("\t<tr><td>IP:</td><td>"));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"i0\" name=\"i0\" value=\"")); client.print(ip[0],DEC); client.println(F("\">."));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"i1\" name=\"i1\" value=\"")); client.print(ip[1],DEC); client.println(F("\">."));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"i2\" name=\"i2\" value=\"")); client.print(ip[2],DEC); client.println(F("\">."));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"i3\" name=\"i3\" value=\"")); client.print(ip[3],DEC); client.println(F("\">"));
        client.println(F("\t</td></tr>"));
 
        client.println(F("\t<tr><td>SUBNET:</td><td>"));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"s0\" name=\"s0\" value=\"")); client.print(subnet[0],DEC); client.println(F("\">."));    
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"s1\" name=\"s1\" value=\"")); client.print(subnet[1],DEC); client.println(F("\">."));       
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"s2\" name=\"s2\" value=\"")); client.print(subnet[2],DEC); client.println(F("\">."));    
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"s3\" name=\"s3\" value=\"")); client.print(subnet[3],DEC); client.println(F("\">"));
        client.println(F("\t</td></tr>"));
 
        client.println(F("\t<tr><td>GW:</td><td>"));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"g0\" name=\"g0\" value=\"")); client.print(ip_gateway[0],DEC); client.println(F("\">."));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"g1\" name=\"g1\" value=\"")); client.print(ip_gateway[1],DEC); client.println(F("\">."));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"g2\" name=\"g2\" value=\"")); client.print(ip_gateway[2],DEC); client.println(F("\">."));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"g3\" name=\"g3\" value=\"")); client.print(ip_gateway[3],DEC); client.println(F("\">"));
        client.println(F("\t</td></tr>"));
 
        client.println(F("\t<tr><td>DNS:</td><td>"));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"d0\" name=\"d0\" value=\"")); client.print(ip_dns[0],DEC); client.println(F("\">."));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"d1\" name=\"d1\" value=\"")); client.print(ip_dns[1],DEC); client.println(F("\">."));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"d2\" name=\"d2\" value=\"")); client.print(ip_dns[2],DEC); client.println(F("\">."));
        client.print(F("\t\t<input type=\"text\" size=\"3\" maxlength=\"3\" id=\"d3\" name=\"d3\" value=\"")); client.print(ip_dns[3],DEC); client.println(F("\">"));
        client.println(F("\t</td></tr>"));
 
        client.println(F("\t<tr><td colspan=\"2\" style=\"background-color: #999999;\">Usuarios</td></tr>"));
 
        String sUserPass = rbase64.decode(String(cUserPass64));
        String sPass = sUserPass.substring(6,String(cUserPass).length());
 
        client.println(F("\t<tr><td>USUARIO:</td><td>admin</td></tr>"));
        client.println(F("\t<tr><td>CONTRASEÑA:</td><td>"));
        client.print(F("\t\t<input type=\"password\" size=\"16\" maxlength=\"16\" id=\"password\" name=\"password\" value=\"")); client.print(sPass); client.println(F("\">"));
        client.println(F("\t</td></tr>"));
 
        client.println(F("\t<tr><td>&nbsp;</td></tr>"));
 
        client.println(F("<tr><td colspan=\"2\">"));
        client.println(F("\t\t<input id=\"button1\" type=\"submit\" value=\"GUARDAR\" />"));
        client.println(F("\t</td></tr>"));
        client.print(F("</table>\n</form>\n</body>\n</html>")); 
      }
    }
  }else{
    client.httpUnauthorized();
  }
}
 
void setup(){
  pinMode(switchPin, INPUT);
 
  ShieldSetup (); 
  webserver.setDefaultCommand(&defaultCmd);
  webserver.addCommand("index.html", &defaultCmd);
  webserver.addCommand("setup.html", &privateCmd);
  webserver.begin();
}
 
void ShieldSetup(){
  //  Serial.begin(9600);
  int idcheck = EEPROM.read(0);
 
  if (idcheck == ID_KNOW){
    mac[0] = EEPROM.read(1); mac[1] = EEPROM.read(2); mac[2] = EEPROM.read(3); mac[3] = EEPROM.read(4); mac[4] = EEPROM.read(5); mac[5] = EEPROM.read(6); 
    ip[0] = EEPROM.read(7); ip[1] = EEPROM.read(8); ip[2] = EEPROM.read(9); ip[3] = EEPROM.read(10); 
    subnet[0] = EEPROM.read(11); subnet[1] = EEPROM.read(12); subnet[2] = EEPROM.read(13); subnet[3] = EEPROM.read(14); 
    ip_gateway[0] = EEPROM.read(15); ip_gateway[1] = EEPROM.read(16); ip_gateway[2] = EEPROM.read(17); ip_gateway[3] = EEPROM.read(18); 
    ip_dns[0] = EEPROM.read(19); ip_dns[1] = EEPROM.read(20); ip_dns[2] = EEPROM.read(21); ip_dns[3] = EEPROM.read(22); 
    for (int i=0;i<43;i++){
      cUserPass64[i] = EEPROM.read(i+23); 
    }
  }
  Ethernet.begin(mac, ip, ip_dns, ip_gateway, subnet);
}
 
void loop(){
  char buff[64];
  int len = 64;
 
  webserver.processConnection(buff, &len);  
 
  // RESET A FABRICA CON SWITCH EN PIN 2
  prevValueReset = valueReset;
  valueReset = digitalRead(switchPin);
 
  if (valueReset == HIGH && prevValueReset == LOW) {                // Si oprimo el boton
    prevMillisReset = millis(); 
  }else if (valueReset == LOW && prevValueReset == HIGH) {          // Si suelto el boton 
    if(millis()-prevMillisReset>3000){                              // Si es mayor a 3 segundos Reset a Fabrica
      EEPROM.write(0, 0x00);   
    }                                                               // Reseteo el equipo
    soft_restart();
  }
}