Desde el punto de vista del software, establecer una conexión Ethernet con Arduino es muy sencillo. Para hacerlo se utiliza la librería Ethernet. Esta librería está diseñada para una Ethernet Shield que está basada en el integrado W5100, pero existen otras placas o módulos diferentes y/o que utilizan otros integrados, como el ENC28J60. Para simplificar su uso y aumentar la compatibilidad, otras librerías utilizan (casi) la misma API que la librería Ethernet, sólo habrá que sustituir la librería alternativa por la original o incluirla (cuando el nombre sea diferente) en su lugar aunque en el código se usen las mismas (o muy parecidas) funciones. En mi caso, utilizo la librería UIPEthernet de Norbert Truchsess siguiendo el mismo proceso que voy a describir en este texto.
1. Definir la conexión Ethernet
Tanto si se va a adoptar el papel de cliente como el de servidor, en primer lugar hay que definir la conexión con la función begin() a la que se le puede pasar como parámetro sólo la dirección MAC y esperar que un servidor DHCP en la red le asigne una dirección IP y el resto de la configuración o también es posible indicar (opcionalmente) más parámetros hasta definir la configuración completa:
- Dirección MAC (la que ya se ha citado)
- Dirección IP del shield o módulo
- Dirección IP del servidor DNS (sólo un servidor)
- Dirección IP de la puerta de enlace
- Máscara de red
Es recomendable indicar todos los parámetros, salvo que su deducción sea la habitual, para evitar que la configuración no sea correcto (por ejemplo, que la pasarela no sea la primera dirección de la red)
De lo anterior parece que queda claro que hay que usar bastantes veces datos que representen direcciones IP, por eso la librería incluye la clase IPAddress de la que instanciar objetos dirección IP. Los parámetros que la definen son los cuatro bytes de una dirección IPV4
La dirección MAC se define para esta librería como una matriz de 6 bytes. La dirección MAC es (se supone que es) un identificador único en el que los primeros bytes indican el fabricante y el modelo y los últimos al dispositivo en concreto. El integrado ENC28J60 no incluye una dirección MAC salvo que se opte por comprar además un integrado de dirección MAC de Microchip (o todo un bloque OUI de direcciones al IEEE si la tirada de dispositivos es lo bastante grande como para que merezca la pena). Cuando no se dispone de una dirección MAC se puede inventar una cuidando que no entre en conflicto con otras en la red en la que se encuentre el dispositivo.
Si la configuración se realiza con un servidor DHCP en lugar de «a mano», la función localIP() es útil para consultar la dirección que el servidor le ha asignado al módulo. Para renovar la dirección asignada (si el tiempo correspondiente hubiera expirado) la librería Ethernet proporciona la función maintain() que además informará devolviendo un código que corresponde con el estado de la renovación:
- La operación no ha tenido ningún efecto
-
Error al renovar (renew) la dirección IP
No se ha podido prolongar el uso de la dirección IP asignada en el mismo servidor - Dirección IP renovada correctamente
-
Error al reasignar (rebind) la dirección IP
No se ha podido prolongar el uso de la dirección IP asignada en ningún servidor - Dirección IP reasignada correctamente
Con la información vista hasta ahora ya se puede escribir un ejemplo de cómo se iniciaría una conexión Ethernet configurando la dirección IP por medio de un servidor DHCP en la red. En el siguiente código de ejemplo se trata de renovar la dirección IP cada cierto periodo de tiempo y se informa del resultado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
//#include <UIPEthernet.h> // Librería Ethernet que usaré después con el módulo ENC28J60
#include <Ethernet.h> // Librería Ethernet estándar
#define ESPERA_RENOVACION_IP 60000 // Un minuto
unsigned long reloj;
byte direccion_mac[]={0x12,0x34,0x56,0x78,0x9a,0xbc}; // Dirección MAC inventada
byte estado_DHCP;
void setup()
{
Serial.begin(9600);
Ethernet.begin(direccion_mac);
mostrar_direccion_ip();
reloj=millis()+ESPERA_RENOVACION_IP;
}
void loop()
{
if(millis()>reloj) // Tratar de renovar la IP cada ESPERA_RENOVACION_IP milisegundos
{
estado_DHCP=Ethernet.maintain();
switch(estado_DHCP)
{
case 0:
Serial.println(«Sin cambios»);
break;
case 1:
Serial.println(«Error al renovar la dirección IP»);
break;
case 2:
Serial.println(«Dirección IP renovada correctamente»);
break;
case 3:
Serial.println(«Error al reasignar la dirección IP»);
break;
case 4:
Serial.println(«Dirección IP reasignada correctamente»);
break;
default:
Serial.println(«Error desconocido»);
}
mostrar_direccion_ip();
reloj=millis()+ESPERA_RENOVACION_IP;
}
}
void mostrar_direccion_ip()
{
Serial.print(«Dirección IP actual [«);
Serial.print(Ethernet.localIP()); // Mostrará la dirección IP asignada por el servidor DHCP
Serial.println(«]»);
}
|
En el ejemplo de abajo se asigna la dirección IP y el resto de la configuración manualmente utilizando objetos IPAddress para que resulte más cómodo leerlo y (en caso de código más complejo) evitar los errores que se podrían producir si se escribiera (mal) la dirección en cada uso.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <UIPEthernet.h> // Librería Ethernet usada con el módulo ENC28J60
// #include <Ethernet.h> // Librería Ethernet estándar
byte direccion_mac[]={0x12,0x34,0x56,0x78,0x9a,0xbc}; // Dirección MAC inventada
IPAddress direccion_ip_fija(192,168,1,69); // Dirección IP elegida para el módulo
IPAddress servidor_dns(87,216,170,85); // Servidor DNS OpenNIC (de Alejandro Bonet, http://opennic.alargador.org)
IPAddress puerta_enlace(192,168,1,14); // Dirección IP del router
IPAddress mascara_red(255,255,255,0); // Máscara de la red
void setup()
{
Serial.begin(9600);
while(!Serial){;} // He usado una placa Leonardo, me toca esperar a que el puerto serie esté operativo
Ethernet.begin(direccion_mac,direccion_ip_fija,servidor_dns,puerta_enlace,mascara_red);
Serial.print(«Dirección IP asignada [«);
Serial.print(Ethernet.localIP()); // Poco misterio, devolverá la dirección IP asignada manualmente
Serial.println(«]»);
}
void loop()
{
// Sólo es un ejemplo de configuración, no hace nada
}
|
2. Iniciar la conexión en modo cliente o servidor
Al iniciar una conexión en modo servidor, es el sistema microcontrolado que se está desarrollando el que queda a la escucha de las peticiones de otros sistemas. Para iniciar la conexión como servidor se utiliza EthernetServer() y se indica como parámetro el puerto en el que el servidor escuchará. EthernetServer() es el constructor de la clase Server, que soporta todas las operaciones Ethernet como servidor. Aunque lo más ortodoxo es realizar una llamada al constructor EthernetServer(), no es raro encontrar algunos ejemplos que usan directamente la clase Server o librerías alternativas para conexión Ethernet que eligen usar ese sistema de instanciado.
La conexuión como cliente es la que realiza las peticiones al sistema servidor que es el que las espera y las contesta según corresponda. Para inicar una conexión como cliente se utiliza EthernetClient() que es el constructor de la clase Client origen de todas las operaciones Ethernet como cliente.
A diferencia de lo que ocurre con el modo servidor, que se supone funcionando desde que se instancia la clase (aunque responderá a los clientes sólo si lo está realmente), se debe verificar que la conexión cliente está preparada antes de utilizarla. El objeto cliente que se crea al iniciar la conexión puede consultarse para verificar si está disponible. Por ejemplo, las operaciones de consulta pueden incluirse en una estructura if(EthernetClient) para ejecutarlas sólo cuando la conexión cliente esté disponible.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <UIPEthernet.h> // Librería Ethernet usada con el módulo ENC28J60
// #include <Ethernet.h> // Librería Ethernet estándar
byte direccion_mac[]={0x12,0x34,0x56,0x78,0x9a,0xbc}; // Dirección MAC inventada
IPAddress direccion_ip_fija(192,168,1,69); // Dirección IP elegida para el módulo
IPAddress servidor_dns(87,216,170,85); // Servidor OpenNIC (de Alejandro Bonet, http://opennic.alargador.org)
IPAddress puerta_enlace(192,168,1,14); // Dirección IP del router
IPAddress mascara_red(255,255,255,0); // Máscara de la red
EthernetServer servidor=EthernetServer(80); // Puerto 80 (típico de un servidor HTTP)
void setup()
{
Serial.begin(9600);
while(!Serial){;} // He usado una placa Leonardo, hay que esperar a que el puerto serie esté operativo
Ethernet.begin(direccion_mac,direccion_ip_fija,servidor_dns,puerta_enlace,mascara_red);
servidor.begin();
Serial.println(«Servidor HTTP iniciado»);
}
void loop()
{
// Sólo es un ejemplo de configuración, no hace nada productivo
}
|
3. Establecer una conexión como cliente
Como se ha dicho, una vez creada la conexión, es el cliente el que toma la iniciativa de realizar las consultas. El servidor estará esperando esa iniciativa y responderá como proceda. Es, por tanto, el cliente el que conecta al servidor, para hacerlo se utiliza connect() indicando como parámetros el servidor (la dirección IP o la URL) y el puerto en el que escucha.
Según el resultado de la operación, la función devolverá los valores
- ( ) Conexión establecida correctamente
- Estableciendo la conexión
- ( ) Ha pasado el tiempo de espera sin que se establezca la conexión
- ( ) No se ha encontrado el servidor o no responde correctamente
- ( ) La conexión se ha cortado antes de establecerse completamente
- ( ) La respuesta del servidor es incorrecta
Antes de empezar a realizar consultas es necesario verificar que la conexión está operativa con la función connected() que devolverá si ya está disponible o en caso contrario.
El ejemplo de abajo ilustra la conexión como cliente verificando cada 10 segundos si existe conexión con el servidor (no pretende ser nada productivo, sólo mostrar la sintaxis de las funciones) algo que, por cierto, no gustaría mucho a un servidor web en producción.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
#include <UIPEthernet.h> // Librería Ethernet usada con el módulo ENC28J60
// #include <Ethernet.h> // Librería Ethernet estándar
#define INTERVALO_CONSULTA 10000 // Se comprueba cada 10 segundos si hay conexión
#define LED_CONEXION 13 // Pin del LED que parpadea cuando hay conexión
#define TIEMPO_PARPADEO 300 // Milisegundos entre encendido/apagado del LED que indica conexión
byte direccion_mac[]={0x12,0x34,0x56,0x78,0x9a,0xbc}; // Dirección MAC inventada
IPAddress direccion_ip_fija(192,168,1,69); // Dirección IP elegida para el módulo
IPAddress servidor_dns(87,216,170,85); // Servidor OpenNIC (de Alejandro Bonet, http://opennic.alargador.org)
IPAddress puerta_enlace(192,168,1,14); // Dirección IP del router
IPAddress mascara_red(255,255,255,0); // Máscara de la red
IPAddress ip_servidor_web(192,168,1,21); // Dirección IP del servidor web (en la intranet)
//char url_servidor_web[]=»sleepmanager.onironauta.es»; // URL poético para un gestor de sueño (en Internet)
EthernetClient cliente=EthernetClient();
byte estado_conexion;
boolean anteriormente_conectado;
boolean estado_led_conexion;
unsigned long cronometro_parpadeo;
unsigned long cronometro_consulta;
void conectar_ethernet()
{
estado_conexion=cliente.connect(ip_servidor_web,80); // Conexión desde la intranet
//estado_conexion=cliente.connect(url_servidor_web,80); // Conexión desde Internet
delay(100); // Un pequeño retraso para permitir que se active la conexión
anteriormente_conectado=false;
switch(estado_conexion)
{
case 1:
Serial.println(«Conexión con el servidor SleepManager establecida correctamente»);
anteriormente_conectado=true;
break;
case –1:
Serial.println(«Ha pasado el tiempo de espera sin que se establezca la conexión»);
break;
case –2:
Serial.println(«No se ha encontrado el servidor o no responde correctamente»);
break;
case –3:
Serial.println(«La conexión se ha interrumpido antes de establecerse completamente»);
break;
case –4:
Serial.println(«La respuesta del servidor es incorrecta»);
break;
}
}
void setup()
{
pinMode(LED_CONEXION,OUTPUT);
Serial.begin(9600);
while(!Serial){;} // Esperar al puerto serie de la placa Leonardo
Serial.println(«Conectando con el servidor SleepManager…»);
Ethernet.begin(direccion_mac,direccion_ip_fija,servidor_dns,puerta_enlace,mascara_red);
conectar_ethernet();
estado_led_conexion=false;
cronometro_parpadeo=0;
cronometro_consulta=millis()+INTERVALO_CONSULTA;
}
void loop()
{
if(anteriormente_conectado&&millis()>cronometro_parpadeo)
{
estado_led_conexion=!estado_led_conexion;
digitalWrite(LED_CONEXION,estado_led_conexion);
cronometro_parpadeo=millis()+TIEMPO_PARPADEO;
}
if(millis()>cronometro_consulta)
{
if(!cliente.connected())
{
conectar_ethernet();
}
cronometro_consulta=millis()+INTERVALO_CONSULTA;
}
}
|
4. Enviar datos
Igual que otras clases más conocidas, como ocurre con Serial, y con un uso equiparable, las clases Client y Server disponen de las funciones
-
Envía información usando el objeto cliente o servidor desde el que se invoque. El parámetro «dato» es un único byte o char mientras que «buffer» es una matriz de byte o char de la que se envía una cantidad igual a «longitud» Esta función es la que se utiliza para las operaciones binarias, frente a las dos siguientes que suelen reservarse para enviar texto.
-
Envía como cliente o servidor (según la clase desde la que se use) la información correspondiente a «dato» como texto. Si la información no está expresada como texto (por ejemplo es un número entero) puede utilizarse el parámetro opcional «base» con el que elegir la de la conversión que podrá ser una de las constantes BIN, OCT, DEC o HEX que indican, respectivamente las bases correspondientes a binario (base 2), octal (base 8), decimal (base 10) y hexadecimal (base 16)
-
El funcionamiento es idéntico a la anterior excepto por enviar, después de la información indicada expresamente por el parámetro «dato», un retorno de carro (el código 13 que se puede representar como \r) y un final de línea (el código 10, que se puede representar por \n) Frecuentemente se hace referencia a estos códigos, respectivamente, por las siglas CR (Carriage Return) y LF (Line Feed)
Las tres funciones anteriores devuelven el número de bytes que se han enviado, como ocurre también con las funciones equivalentes de la clase Serial; como se dijo arriba, el funcionamiento es equiparable.
5. Recibir datos
Igual que en el caso de las operaciones de envío de datos, las de recepción son equiparables a las de la ampliamente usada Serial. El protocolo de recepción también es similar: verificar si hay (suficientes) datos disponibles (available) y en tal caso leerlos
-
Devuelve el número de bytes que hay disponibles para ser leídos. Esta función está presente tanto en la clases Client como Server; en el primer caso informa del número de bytes que ha enviado el servidor en respuesta a una petición y que está disponible para que el cliente la lea (read), y en el segundo caso el (objeto) cliente que ha realizado una operación o false en caso de que no haya ninguno.
-
Sirve para leer la información que se ha recibido. Esta función sólo está disponible en la clase Client. Si la aplicación que se está desarollando cumple con el papel de servidor, para leer la información que ha llegado debe instanciarse un objeto cliente con la respuesta de la función available() comentada en el anterior apartado.
El siguiente ejemplo es un «servidor de mayúsculas» que escucha en el puerto 2000 y responde a las peticiones con lo que se haya enviado pasado a mayúsculas cuando sea posible. Puede probarse, por ejemplo con PuTTY o simplemente con Ciertamente no es algo muy práctico, su finalidad sólo es mostrar cómo obtener en el servidor los datos enviados al mismo por un cliente.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#include <UIPEthernet.h> // Librería Ethernet usada con el módulo ENC28J60
//#include <Ethernet.h> // Librería Ethernet estándar
byte direccion_mac[]={0x12,0x34,0x56,0x78,0x9a,0xbc}; // Dirección MAC inventada
IPAddress direccion_ip_fija(192,168,1,69); // Dirección IP elegida para el módulo
IPAddress servidor_dns(87,216,170,85); // Servidor OpenNIC (de Alejandro Bonet, http://opennic.alargador.org)
IPAddress puerta_enlace(192,168,1,14); // Dirección IP del router
IPAddress mascara_red(255,255,255,0); // Máscara de la red
EthernetServer servidor=EthernetServer(2000);
EthernetClient cliente;
char texto_recibido; // Sólo para que se más fácil leer el programa usando varias líneas de código
void setup()
{
Serial.begin(9600);
while(!Serial){;} // Esperar al puerto serie de la placa Leonardo esté operativo
Ethernet.begin(direccion_mac,direccion_ip_fija,servidor_dns,puerta_enlace,mascara_red);
Serial.println(«Iniciando el servidor de mayúsculas»);
servidor.begin();
}
void loop()
{
cliente=servidor.available();
// Si hay disponible alguna petición de un cliente leerla y devolverla en mayúsculas
if(cliente)
{
texto_recibido=cliente.read();
if(texto_recibido>96&&texto_recibido<123) // Si se recibe una letra minúscula…
{
texto_recibido-=32; // …convertirla a mayúsculas
}
// Si se tiene la seguridad de recibir texto se puede usar print en lugar de write
servidor.write(texto_recibido); // Responder con lo recibido pasado a mayúsculas si procede
}
}
|
6. Finalizar la conexión
Mientras que lo habitual es que una aplicación servidor funcione indefinidamente, las conexiones cliente se establece, realizan conexiones y terminan, lo que permite recuperar recursos y emplearlos en otras conexiones o dedicarlos a otros usos del programa. La función stop() de la clase Client se utiliza para terminar una conexión cliente y liberar los recursos que esté utilizando.
De cara al servidor, que el cliente termine la conexión cuando se ha enviado o recibido la información objeto de la consulta también le permite liberar recursos para destinarlos a otras conexiones o distintos fines. En definitiva, aunque parece algo menor, es conveniente terminar la conexión al terminar las operaciones del cliente.
Otra buena práctica al terminar una conexión cliente es vaciar el que utiliza la clase. Para hacerlo se dispone de la función flush() a la debería llamarse después de terminar la conexión cliente con stop()
Ejemplo de consulta HTTP GET
Para aclarar mejor todo lo anterior a continuación se incluye un ejemplo más completo de peticiones TCP usando el peticiones GET usando el protocolo HTTP. En el ejemplo se envían los los valores obtenidos por unos sensores analógicos conectados a una placa Arduino a un servidor web que los almacena en una base de datos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
#include <UIPEthernet.h> // Librería Ethernet usada con el módulo ENC28J60
// #include <Ethernet.h> // Librería Ethernet estándar
#define INTERVALO_CONSULTA 60000 // Enviar datos cada minuto
#define INTERVALO_RECONEXION 10000 // Reintentar la conexión 10 segundos más tarde si no ha sido posible hacerlo cuado correspondía
#define CANTIDAD_SENSORES 6 // Número de sensores analógicos empezando en A0
byte direccion_mac[]={0x12,0x34,0x56,0x78,0x9a,0xbc}; // Dirección MAC inventada
IPAddress direccion_ip_fija(192,168,1,69); // Dirección IP elegida para el módulo
IPAddress servidor_dns(87,216,170,85); // Servidor OpenNIC (de Alejandro Bonet, http://opennic.alargador.org)
IPAddress puerta_enlace(192,168,1,14); // Dirección IP del router
IPAddress mascara_red(255,255,255,0); // Máscara de la red
//IPAddress ip_servidor_web(192,168,1,21); // Dirección IP del servidor web (en la intranet)
char url_servidor_web[]=«sleepmanager.onironauta.es»; // URL poético para un gestor de sueño (en Internet)
EthernetClient cliente;
byte estado_conexion;
String texto_consulta;
unsigned long cronometro_consulta;
byte contador;
void setup()
{
Serial.begin(9600);
while(!Serial){;} // Esperar al puerto serie de la placa Leonardo
Serial.println(«Conectando con el servidor SleepManager…»);
Ethernet.begin(direccion_mac,direccion_ip_fija,servidor_dns,puerta_enlace,mascara_red);
cronometro_consulta=millis()+INTERVALO_CONSULTA;
}
void loop()
{
if(millis()>cronometro_consulta)
{
//estado_conexion=cliente.connect(ip_servidor_web,80); // Conexión desde la intranet
estado_conexion=cliente.connect(url_servidor_web,80); // Conexión desde Internet
while(estado_conexion==0) // esperar a que se establezca la conexión o se produzca un error
{
switch(estado_conexion)
{
case 1:
Serial.println(«Conexión con el servidor SleepManager establecida correctamente»);
break;
case –1:
Serial.println(«Ha pasado el tiempo de espera sin que se establezca la conexión»);
break;
case –2:
Serial.println(«No se ha encontrado el servidor o no responde correctamente»);
break;
case –3:
Serial.println(«La conexión se ha interrumpido antes de establecerse completamente»);
break;
case –4:
Serial.println(«La respuesta del servidor es incorrecta»);
break;
}
}
if(cliente.connected()) // Si ha sido posible conectar realizar la consulta
{
cronometro_consulta=millis()+INTERVALO_CONSULTA;
texto_consulta=«GET /pruebas/guardar_sensores_analogicos.php?origen=SleepManager»;
for(contador=0;contador<CANTIDAD_SENSORES;contador++)
{
texto_consulta=«&sensor_»+String(contador+1,DEC)+«=»+String(analogRead(contador),DEC);
delay(1); // Como tarda 100 μs en obtener el valor analógico, con 1 ms seguro le da tiempo
}
texto_consulta+=» HTTP/1.1\r\nHost: «+String(url_servidor_web)+«\r\nUser-Agent: sleep_inspector\r\n\r\n»;
cliente.print(texto_consulta);
cliente.flush();
cliente.stop();
}
else // Si no ha sido posible conectar reintentarlo más tarde pero no tanto como si hubiera sido posible hacerlo
{
cronometro_consulta=millis()+INTERVALO_RECONEXION;
}
}
}
|
Publicar comentario