Skip to content

5. Tarea 2 - Control de Arduinos con una tableta Android

Ahora aprenderemos a controlar una placa Arduino con una tableta. El ejemplo a seguir es el proyecto [client-android-skel] del curso (ver párrafo 2).

5.1. Arquitectura de proyectos

Todo el proyecto tendrá la siguiente arquitectura:

  • El bloque [1], el servidor web/JSON y los Arduinos, te serán proporcionados;
  • usted tendrá que construir el bloque [2], el programa de la tableta Android para comunicarse con el servidor web / JSON.

5.2. Hardware

Tiene a su disposición los siguientes componentes:

  • un Arduino con un escudo Ethernet, un LED, y un sensor de temperatura;
  • un miniHub para compartir con otro alumno;
  • un cable USB para alimentar el Arduino;
  • dos cables de red para conectar el Arduino y el PC a la misma red privada;
  • una tableta Android;

5.2.1. El Arduino

A continuación te explicamos cómo conectar los distintos componentes:

  • desconecte el cable de red de su PC;
  • Conecta tu PC y el Arduino mediante un cable de red;
  • El Arduino que tienes ya estará programado. Su dirección IP será [192.168.2.2]. Para que tu PC reconozca el Arduino, deberás asignarle una dirección IP en la red [192.168.2]. Los Arduinos han sido programados para comunicarse con un PC con la dirección IP [192.168.2.1]. He aquí cómo hacerlo:

Vaya a [Panel de control\NRed e Internet\NCentro de redes y recursos compartidos]:

 
  • En [1], haga clic en el enlace [Red de área local];
  • en [2], haga clic en el botón [Propiedades] de la red local;
  • en [3], haga clic en las propiedades [IPv4] del adaptador [Conexión de área local];
  • en [4], asigna a este adaptador la dirección IP [192.168.2.1] y la máscara de subred [255.255.255.0];
  • en [5], haga clic en [OK] tantas veces como sea necesario para salir del asistente.

5.2.2. La tableta

  • Utilizando su adaptador Wi-Fi, conecte su ordenador a la red Wi-Fi que le proporcionaremos. Haga lo mismo con su tableta;
  • Comprueba la dirección Wi-Fi PC de tu IP escribiendo [ipconfig] en una ventana de símbolo del sistema. Encontrará una dirección como [192.168.x.y];

dos>ipconfig
 
Windows IP Configuration
 
Wi-Fi wireless network adapter:
 
   Connection-specific DNS suffix . . . :
   Local-link IPv6 address. . . . .: fe80::39aa:47f6:7537:f8e1%2
   IPv4 address. . . . . . . . . . . . . .: 192.168.1.25
   Subnet mask. . . . . . . . . : 255.255.255.0
   Default gateway. . . . . . . . . : 192.168.1.1
  • Comprueba la dirección IP Wi-Fi de tu tableta. Pregunta a tu instructor cómo hacerlo si no estás seguro. Encontrarás una dirección como [192.168.x.z];
  • Desactive el cortafuegos de su PC si está activo [Panel de control\Sistema y seguridad\Windows Firewall];
  • En una ventana de símbolo del sistema, compruebe que el PC y la tableta pueden comunicarse escribiendo el comando [ping 192.168.x.z], donde [192.168.x.z] es la dirección IP de la tableta. La tableta debería responder:
dos>ping 192.168.1.26

Sending a 'Ping' request to 192.168.1.26 with 32 bytes of data:
Response from 192.168.1.26: bytes=32 time=102 ms TTL=64
Response from 192.168.1.26: bytes=32 time=134 ms TTL=64
Response from 192.168.1.26: bytes=32 time=168 ms TTL=64
Response from 192.168.1.26: bytes=32 time=208 ms TTL=64

Ping statistics for 192.168.1.26:
    Packets: sent = 4, received = 4, lost = 0 (0% loss),
Approximate round-trip times in milliseconds:
    Minimum = 102 ms, Maximum = 208 ms, Average = 153 ms

La configuración de red de su sistema ya está lista.

5.2.3. El emulador [Genymotion]

El emulador [Genymotion] (véase la sección 6.9) es una gran alternativa a una tableta. Es casi igual de rápida y no requiere conexión Wi-Fi. Recomendamos utilizar este método. Puedes utilizar la tableta para las pruebas finales de tu aplicación.

5.3. Programación de Arduinos

Aquí nos centraremos en escribir código C para Arduinos:

Ver también

  • Instalación del IDE de desarrollo de Arduino (véase la sección 6.1);
  • Utilización de las bibliotecas JSON (Apéndices, Sección 6.6);
  • En el Arduino IDE, prueba el ejemplo de un servidor TCP (e.g., el servidor web) y el de un cliente TCP (e.g., el cliente Telnet);
  • los apéndices sobre el entorno de programación Arduino en la sección 6.1.

Un Arduino es un conjunto de pines conectados a un hardware. Estos pines son entradas o salidas. Sus valores son binarios o analógicos. Para controlar el Arduino, hay dos operaciones básicas:

  • escribir un binario/analógico valor a un pin identificado por su número;
  • leer a binario/analógico valor de una clavija identificada por su número;

A estas dos operaciones básicas, añadiremos una tercera:

  • hacer parpadear un LED durante un tiempo y con una frecuencia determinados. Esta operación puede realizarse llamando repetidamente a las dos operaciones básicas anteriores. Sin embargo, veremos en las pruebas que los intercambios entre la capa [DAO] y un Arduino duran del orden de un segundo. Por lo tanto, no es posible hacer que un LED parpadee cada 100 milisegundos, por ejemplo. Así que implementaremos esta función de parpadeo en el propio Arduino.

El Arduino funcionará de la siguiente manera:

  • La comunicación entre la capa [DAO] y un Arduino tiene lugar a través de una red TCP-IP mediante el intercambio de líneas de texto en formato JSON (JavaScript Object Notation);
  • al arrancar, el Arduino se conecta al puerto 100 de un servidor de registro situado en la capa [DAO]. Envía una única línea de texto al servidor:
{"id":"kitchen","desc":"duemilanove","mac":"90:A2:DA:00:1D:A7","port":102}

Esta es una cadena JSON que describe el Arduino que se está conectando:

  • id: un identificador para el Arduino;
  • desc: una descripción de lo que puede hacer el Arduino. Aquí, simplemente hemos especificado el modelo de Arduino;
  • mac: la dirección MAC del Arduino;
  • puerto: el número de puerto en el que el Arduino esperará órdenes de la capa [DAO].

Toda esta información está en formato de cadena excepto el puerto, que es un número entero.

  • Una vez que el Arduino se ha registrado con el servidor de registro, comienza a escuchar en el puerto que especificó al servidor (102 arriba). Espera comandos JSON en el siguiente formato:
{"id":"identifier","ac":"an_action","pa":{"param1":"value1","param2":"value2",...}}

Se trata de una cadena JSON con los siguientes elementos:

  • id: un identificador para el comando. Puede ser cualquier cosa;
  • ac: una acción. Hay tres:
  • pw (escribir pin) para escribir un valor en un pin,
  • pr (pin leído) para leer el valor de un pin,
  • cl (parpadear) para hacer parpadear un LED;
  • pa: los parámetros de la acción. Estos dependen de la acción.
  • El Arduino siempre devuelve una respuesta a su cliente. Se trata de una cadena JSON con el siguiente formato:
{"id":"1","er":"0","et":{"pinx":"valx"}}

donde

  • id: el identificador del comando al que se responde;
  • er (error): un código de error si se ha producido un error, 0 en caso contrario;
  • et (estado): un diccionario que siempre está vacío excepto para el comando de lectura pr. El diccionario contiene entonces el valor del número de pin x que se solicitó.

He aquí algunos ejemplos para aclarar las especificaciones anteriores:

Haga que LED #8 parpadee 10 veces con un periodo de 100 milisegundos:

Comando
{"id":"1","ac":"cl","pa":{"pin":"8","dur":"100","nb":"10"}}
Respuesta
{"id":"1","er":"0","et":{}}

Los parámetros del cl son: la duración (dur) de un destello en milisegundos, el número (nb) de destellos, y el pin número del LED.

Escriba el valor binario 1 en el pin 7:

Comando
{"id":"2","ac":"pw","pa":{"pin":"7","mod":"b","val":"1"}}
Respuesta
{"id":"2","er":"0","et":{}}

En pa parámetros del pw son: el modo de escritura mod (b para binario o a para analógico), el valor val a escribir, y el pin número. Para una escritura binaria, val es 0 ó 1. Para una escritura analógica, val está en el rango [0,255].

Escribe el valor analógico 120 en el pin 2:

Comando
{"id":"3","ac":"pw","pa":{"pin":"2","mod":"a","val":"120"}}
Respuesta
{"id":"3","er":"0","et":{}}

Lee el valor analógico del pin 0:

Comando
{"id":"4","ac":"pr","pa":{"pin":"0","mod":"a"}}
Respuesta
{"id":"4","er":"0","et":{"pin0":"1023"}}

En pa parámetros del pr son: la lectura modo (binario o analógico), y el pin número. Si no hay error, el Arduino coloca el valor del pin solicitado en el campo "et" clave de su respuesta. Toma, pin0 indica que se ha solicitado el valor del pin 0, y 1023 es ese valor. En modo lectura, un valor analógico estará en el rango [0, 1024].

Hemos introducido los tres comandos cl, pw, y pr. Cabe preguntarse por qué no hemos utilizado campos más explícitos en las cadenas JSON, como por ejemplo acción en lugar de ac, pinwrite en lugar de pw, y parámetros en lugar de pa. Un Arduino tiene una memoria muy limitada. Sin embargo, las cadenas JSON intercambiadas con el Arduino contribuyen al uso de memoria. Por lo tanto, hemos optado por acortarlas tanto como sea posible.

Veamos ahora algunos casos de error:

Comando
xx
Respuesta
{"id":"","er":"100","et":{}}

Se ha enviado un comando que no está en formato JSON. El Arduino ha devuelto el código de error 100.

Comando
{"id":"4","ac":"pr","pa":{"mod":"a"}}
Respuesta
{"id":"4","er":"302","et":{}}

A pr se envió sin el comando pin parámetro. El Arduino devolvió el código de error 302.

Comando
{"id":"4","ac":"pinread","pa":{"pin":"0","mod":"a"}}
Respuesta
{"id":"4","er":"104","et":{}}

Enviamos a un desconocido pinread (es pr). El Arduino devolvió el código de error 104.

No vamos a seguir con los ejemplos. La regla es simple. El Arduino no debe colapsar, independientemente del comando que se le envíe. Antes de ejecutar un comando JSON, se asegura de que el comando es válido. Tan pronto como se produce un error, el Arduino deja de ejecutar el comando y devuelve la cadena de error JSON a su cliente. Una vez más, debido a que el espacio de memoria es limitado, devolvemos un código de error en lugar de un mensaje completo.

El código del programa que se ejecuta en el Arduino se proporciona en los ejemplos de este documento:

  

Para transferirlo al Arduino:

  • Conéctalo a tu PC;
  • en [1], abra el archivo [arduino_uno.ino]. El Arduino IDE se iniciará y cargará el archivo;

Nota: El código fue originalmente creado y probado con Arduino IDE 1.5.x. Desde entonces, se han publicado otras versiones del IDE. El código no funciona con Arduino IDE 1.6.x. Parece que hay un problema de compatibilidad entre las versiones 1.6 y 1.5.

  • En [2-4], especifique el tipo de Arduino utilizado;
  • En [5-7], especifique a qué puerto serie del PC está conectado;
  • en [8], carga el programa [arduino_uno] en el Arduino;

El código del programa está muy comentado. Los lectores interesados pueden consultarlo. Simplemente destacamos las líneas de código que configuran la comunicación bidireccional cliente/servidor entre el Arduino y el PC:


#include <SPI.h>
#include <Ethernet.h>
#include <ajSON.h>
 
// ---------------------------------- ARDUINO UNO CONFIGURATION
// Arduino UNO MAC address
byte macArduino[] = { 
  0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// Arduino IP address
IPAddress ipArduino(192, 168, 2, 2);
// its identifier
char * idArduino="kitchen";
// Arduino server port
int ArduinoPort = 102;
// Arduino description
char * descriptionArduino="home automation control";
// The Arduino server will run on port 102
EthernetServer server(ArduinoPort);
// IP address of the registration server
IPAddress recordingServerIP(192, 168, 2, 1); 
// Registration server port
int recordingServerPort = 100;
// Arduino client for the recording server
EthernetClient ArduinoClient;
// client command
char command[100];
// the Arduino's response
char message[100];
 
// initialization
void setup() {
  // The serial monitor will allow you to follow the exchanges
  Serial.begin(9600);
  // Start the Ethernet connection
  Ethernet.begin(macArduino, ipArduino);  
  // Available memory
  Serial.print(F("Available memory: "));
  Serial.println(freeRam());
}
 
// infinite loop
void loop()
{
  ...
}
  • Línea 8: la dirección MAC del Arduino. No importa mucho aquí porque el Arduino estará en una red privada con un PC y uno o más Arduinos. La dirección MAC simplemente necesita ser única en esta red privada. Normalmente, la placa de red del Arduino tiene una pegatina que indica la dirección MAC de la placa. Si esta pegatina no está y no conoces la dirección MAC de la placa, puedes introducir lo que quieras en la línea 8 siempre que se cumpla la regla de que la dirección MAC sea única en la red privada;
  • línea 11: la dirección IP de la tarjeta. De nuevo, puedes introducir cualquier valor de la forma [192.168.2.x] y variar la x para los diferentes Arduinos de la red privada;
  • Línea 13: Identificador del Arduino. Debe ser único entre los identificadores de Arduinos en la misma red privada;
  • línea 15: el puerto de servicio del Arduino. Puedes introducir lo que quieras;
  • línea 17: descripción de la función del Arduino. Puedes introducir lo que quieras. Tenga cuidado con las cadenas largas debido a la memoria limitada del Arduino;
  • línea 21: IP dirección del servidor de registro del Arduino en el PC. No debe modificarse;
  • línea 23: puerto para este servicio de registro. No debe modificarse;

5.4. El servidor Web/JSON

5.4.1. Instalación

Image

Se proporciona el binario Java para el servidor web/jSON:

 

Abra un símbolo del sistema y escriba el siguiente comando:

dos>java -jar arduinos-server-01-all-1.0.jar

If [java.exe] is not in the command prompt’s PATH, you will need to type the full path to [java.exe] (usually C:\Program Files\java\...).

Se abrirá una ventana DOS que mostrará los registros:


.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v0.5.0.M6)
 
2014-01-06 11:11:35.550  INFO 8408 --- [           main] arduino.rest.metier.Application          : Starting Application on Gportpers3 with PID 8408 (C:\Users\SergeTahÚ\Desktop\part2\server.jar started by ST)
2014-01-06 11:11:35.587  INFO 8408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@6a4ba620: startup date [Mon Jan 06 11:11:35 CET 2014]; root of context hierarchy
2014-01-06 11:11:36.765  INFO 8408 --- [           main] org.apache.catalina.core.StandardService   : Starting Tomcat service
2014-01-06 11:11:36.766  INFO 8408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.42
2014-01-06 11:11:36.876  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-01-06 11:11:36.877  INFO 8408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1293 ms
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2014-01-06 11:11:37.184  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.386  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.makeLEDBlink(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/commands/{idArduino}],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.sendCommandesJson(java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.getArduinos(javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.389  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinRead(java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.390  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{value}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinWrite(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.463  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.464  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.881  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 796 ms
Recording server started on 192.168.2.1:100
2014-01-06 11:11:38.101  INFO 8408 --- [       Thread-4] arduino.dao.Recorder                  : Recorder : [11:11:38:101] : [Recording server: waiting for a client]
2014-01-06 11:11:38.142  INFO 8408 --- [           main] arduino.rest.metier.Application : Started Application in 3.257 seconds
  • línea 11: se lanza un servidor Tomcat integrado;
  • línea 15: se carga y ejecuta el servlet Spring MVC [dispatcherServlet];
  • línea 18: el REST URL [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}];
  • línea 19: el REST URL [/arduinos/commands/{idArduino}];
  • línea 20: el REST URL [/arduinos/];
  • línea 21: el REST URL [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}];
  • línea 22: el REST URL [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}];
  • línea 26: se inicia el servidor de registro de Arduino;

Conecta tu Arduino al PC si aún no lo has hecho. El cortafuegos del PC debe estar desactivado. A continuación, en un navegador web, introduzca el URL [http://localhost:8080/arduinos]:

Deberías ver aparecer el ID del Arduino conectado. Si no aparece nada, prueba a reiniciar el Arduino. Tiene un botón de reinicio para este propósito.

El servidor web/JSON ya está instalado.

5.4.2. El URLs expuesto por el servicio web/JSON

Véase: proyecto [Ejemplo-15] (véase la sección 1.16.1);

El servicio web/JSON fue implementado usando Spring MVC y expone lo siguiente URLs:


@Controller
public class WebController {
 
  // business layer
  @Autowired
  private IBusinessModel businessModel;
 
  // list of Arduinos
  @RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String getArduinos() throws JsonProcessingException {
    ...
  }
 
  // blinking
  @RequestMapping(value = "/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String makeLEDBlink(@PathVariable("commandId") String commandId, @PathVariable("ArduinoId") String ArduinoId, @PathVariable("pin") int pin, @PathVariable("duration") int duration, @PathVariable("count") int count) throws JsonProcessingException {
...
  }
 
  // sending JSON commands
  @RequestMapping(value = "/arduinos/commands/{idArduino}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String sendJSONCommands(@PathVariable("idArduino") String idArduino, HttpServletRequest request) throws IOException {
    ...
  }
 
  // read pin
  @RequestMapping(value = "/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String pinRead(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode) throws JsonProcessingException {
    ....
  }
 
  // write pin
  @RequestMapping(value = "/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{value}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String pinWrite(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode, @PathVariable("value") int value) throws JsonProcessingException {
  ...
  }
}

Las respuestas enviadas por el servidor son representaciones JSON de la siguiente clase [Response<T>]:


package client.android.dao.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any status messages
    private List<String> messages;
    // response body
    private T body;
 
    // constructors
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

El URL [/arduinos] devuelve una respuesta de tipo [Response<List<Arduino>>], donde [Arduino] es la siguiente clase:


package android.arduinos.entities;
 
import java.io.Serializable;
 
public class Arduino implements Serializable {
  // data
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;
 
 // getters and setters
...
}
  • línea 7: [id] es el identificador del Arduino;
  • línea 8: su descripción;
  • línea 9: su dirección MAC;
  • línea 10: su dirección IP;
  • línea 11: el puerto en el que escucha comandos;

El URLs:

  • [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}];
  • [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}] ;
  • [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}] ;
  • [/arduinos/commands/{ArduinoID}];

devuelve una respuesta de tipo [Response<ArduinoResponse>], donde la clase [ArduinoResponse] representa la respuesta estándar de Arduino:


public class ArduinoResponse implements Serializable {
 
  private String json;
  private String id;
  private String error;
  private Map<String, Object> status;
 
  // getters and setters
...
}
  • [json]: la cadena JSON enviada por un Arduino que no ha podido ser decodificada (en caso de error), null de lo contrario;
  • [id]: el identificador del comando al que responde el Arduino;
  • [error]: un código de error, 0 si OK, en caso contrario otra cosa;
  • [estado]: un diccionario que contiene la respuesta específica del comando. Normalmente está vacío a menos que el comando solicite la lectura de un valor del Arduino, en cuyo caso ese valor se colocará en este diccionario;

5.4.3. Pruebas de servicios web / JSON

Familiarícese con el servidor web / JSON probando el siguiente URLs:

URL
role
http://localhost:8080/arduinos/
Returns the list of connected Arduinos
http://localhost:8080/arduinos/
blink/1/kitchen/8/100/20/
makes the LED on pin 8
 on the Arduino identified as "cuisine,"
 20 times every 100 ms.
http://localhost:8080/arduinos/
pinRead/1/cuisine/0/a/
analog reading from pin 0 of
 the Arduino identified as "kitchen"
http://localhost:8080/arduinos/
pinRead/1/kitchen/5/b/
binary reading of pin 5 of
 the Arduino identified by cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/8/b/1/
Write the binary value 1 to pin 8 of the Arduino identified by
 cuisine
http://localhost:8080/arduinos/
pinWrite/1/kitchen/4/a/100/
Analog writing of the value 100 to pin 4 of the Arduino identified
 by cuisine

Aquí tienes algunas capturas de pantalla de lo que deberías ver:

Obtener la lista de Arduinos conectados:

La cadena JSON recibida del servidor web / JSON es un objeto con los siguientes campos:

  • [estado]: 0 indica que no hay error; de lo contrario, se ha producido un error;
  • [mensajes]: una lista de mensajes que explican el error si se ha producido un error:
  • [cuerpo]: la lista de Arduinos si no se ha producido ningún error. A continuación, cada Arduino se describe mediante un objeto con los siguientes campos:
    • [id]: el identificador del Arduino. No puede haber dos Arduinos con el mismo identificador;
    • [descripción]: una breve descripción de la funcionalidad del Arduino;
    • [mac]: la dirección MAC del Arduino;
    • [ip]: la dirección IP del Arduino;
    • [puerto]: el puerto en el que escucha comandos;

Haz que el LED del pin 8 del Arduino identificado con [cuisine] parpadee 20 veces cada 100 ms:

 

La cadena JSON recibida del servidor web / JSON es un objeto con los siguientes campos:

  • [estado]: 0 indica que no hay error; de lo contrario, se ha producido un error;
  • [mensajes]: una lista de mensajes que explican el error si se ha producido un error:
  • [cuerpo]: la respuesta del Arduino si no hubo error:
    • [id]: identificador del comando. Este identificador es el 1 en [/blink/1]. El Arduino incluye este identificador de comando en su respuesta;
    • [error]: un número de error. Un valor distinto de 0 indica un error;
    • [Estado]: sólo se utiliza para leer un pin. Su valor es el valor del pin;
    • [json]: se utiliza sólo en caso de error JSON entre el cliente y el servidor. Su valor es la cadena JSON errónea enviada por el Arduino;

Lectura analógica del pin 0 del Arduino identificado por [cocina]:

 

La cadena JSON recibida del servidor web /json es similar a la anterior, con la excepción del campo [state], que representa el valor del pin 0.

Lectura binaria del pin 5 del Arduino identificado por [cocina]:

 

La cadena JSON recibida del servidor web /json es similar a la anterior.

Escritura binaria del valor 1 en el pin 8 del Arduino identificado por [cocina]:

 

La cadena JSON recibida del servidor web /json es similar a la anterior.

Probar el URL [http://localhost:8080/arduinos/commands/cuisine] es más complicado. El método /jSON del servidor web que gestiona este URL espera una petición POST, que no puede simularse fácilmente utilizando un navegador. Para probar este URL, puede utilizar un navegador Chrome con la extensión [Advanced REST Client] (consulte la sección 6.13):

 
  • en [1], el URL de la web/JSON método a probar;
  • en [2], el método POST para enviar la solicitud;
  • en [3-4], el valor contabilizado es JSON;
  • en [5], contabilizándose la cadena JSON. Fíjese en los corchetes que empiezan y terminan la lista. Aquí, la lista contiene sólo un comando JSON que hace que el pin 8 parpadee 10 veces cada 100 ms;
  • en [6], se envía la solicitud;
 
  • en [7], la respuesta JSON enviada por el servidor. El objeto recibido es un objeto con los dos campos habituales [estado, mensajes] y un campo [cuerpo] cuyo valor es la lista de Arduino respuestas a cada uno de los comandos JSON enviados.

Veamos qué ocurre cuando enviamos un comando JSON con una sintaxis incorrecta al Arduino:

A continuación recibimos la siguiente respuesta:

 

Podemos ver que en la respuesta del Arduino, el código de error es [104], indicando que el comando [xx] no fue reconocido.

5.5. Pruebas de clientes Android

A continuación se proporciona el ejecutable del cliente Android terminado:

  

Utilice el ratón para arrastrar el archivo [app-debug.apk] anterior a un emulador de tableta [GenyMotion]. A continuación, se guardará y se ejecutará. Ejecuta también el servidor web/jSON si aún no lo has hecho. Conecta el Arduino al PC con un LED conectado a él. El cliente Android permite gestionar los Arduinos de forma remota. Muestra las siguientes pantallas al usuario.

La pestaña [CONFIG] permite conectarse al servidor y recuperar la lista de Arduinos conectados:

Image

  • En [1], introduzca la dirección IP [192.168.2.1] asignada a su PC (consulte la sección 5.2).

La pestaña [PINWRITE] permite escribir un valor en un pin de Arduino:

Image

Image

La pestaña [PINREAD] permite leer el valor de un pin de Arduino:

Image

La pestaña [BLINK] permite hacer parpadear un Arduino LED:

Image

La pestaña [COMMAND] permite enviar un comando JSON a un Arduino:

Image

5.6. El cliente Android para el servicio web / JSON

Ahora discutiremos la escritura del cliente Android.

5.6.1. Arquitectura de clientes

La arquitectura del cliente Android será la del proyecto [Example-15] (véase la sección 1.16.2);

  • la capa [DAO] se comunica con el servidor web/JSON;

El cliente Android debe ser capaz de controlar múltiples Arduinos simultáneamente. Por ejemplo, queremos ser capaces de hacer que dos LEDs en dos Arduinos parpadeen al mismo tiempo, no uno tras otro. Por lo tanto, nuestro cliente Android utilizará una tarea asíncrona por Arduino, y estas tareas se ejecutarán en paralelo.

5.6.2. El proyecto cliente de Android Studio

Duplique el proyecto [client-android-skel] (consulte la sección 2) en el proyecto [client-arduinos-01] (si es necesario, revise cómo duplicar un proyecto Gradle en la sección 1.15):

5.6.3. Los cinco puntos de vista de XML

  

Habrá cinco XML vistas:

  • [blink]: para hacer parpadear un LED de Arduino. Está asociado al fragmento [BlinkFragment];
  • [Comandos]: para enviar un comando JSON a un Arduino. Está asociado al fragmento [CommandsFragment];
  • [config]: para configurar el servicio web/JSON URL y recuperar la lista inicial de Arduinos conectados. Está asociado al fragmento [ConfigFragment];
  • [pinread]: para leer el valor binario o analógico de un pin Arduino. Está asociado al fragmento [PinReadFragment];
  • [pinwrite]: para escribir un valor binario o analógico en un pin de Arduino. Está asociado al fragmento [PinWriteFragment];

For now, these five XML views will all have the same empty content:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
 
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
  </RelativeLayout>
</ScrollView>
  • La vista está dentro de un contenedor [RelativeLayout] (líneas 7-10), que a su vez está contenido dentro de un contenedor [ScrollView] (líneas 2-11). Esto garantiza que podamos desplazar la vista si supera el tamaño de la pantalla de una tableta;

Asignación: Cree las cinco vistas XML.


5.6.4. El menú de fragmentos

Sabemos que los fragmentos de un proyecto construido con [client-android-skel] deben estar asociados a un menú, aunque esté vacío. En este caso, la aplicación no tendrá menú. El menú vacío ya está en el proyecto;

  

5.6.5. Los cinco fragmentos de la aplicación

 

Tarea: Duplique el fragmento [DummyFragment] en los cinco fragmentos de la aplicación, como se muestra en [2].


El fragmento [ConfigFragment] tiene el siguiente esqueleto:


package client.android.fragments.behavior;
 
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
 
@EFragment
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {
 
  // Fields inherited from the parent class -------------------------------------------------------
...

Sustituya la línea 10 por la siguiente

@EFragment(R.layout.config)

Tarea: Haga lo mismo para los otros cuatro fragmentos adaptando el atributo [@EFragment] de la clase.


Fragmento
Ver
ConfigFragment

R.layout.config
PinReadFragment

R.layout.pinread
PinWriteFragment

R.layout.pinwrite
CommandsFragment

R.layout.commands
BlinkFragment

R.layout.blink

5.6.6. Estados de los fragmentos

Cada fragmento tendrá un estado.


Tarea: Duplica la clase [DummyFragmentState] cinco veces para crear los cinco estados mostrados en [2].


5.6.7. Personalización de proyectos

 

El paquete [architecture / custom] contiene los elementos personalizables de la arquitectura de la aplicación.

5.6.7.1. La interfaz [IMainActivity]

La interfaz [IMainActivity] define lo que los fragmentos pueden solicitar a la actividad así como las constantes de la aplicación. Esta interfaz será la siguiente:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // access to the session
  ISession getSession();
 
  // change view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // application constants -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum wait time for server response
  int TIMEOUT = 1000;
 
  // wait time before executing the client request
  int DELAY = 000;
 
  // Basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = true;
 
  // loading image
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of fragments
  int FRAGMENTS_COUNT = 5;
 
  // number of views
  int VIEW_CONFIG = 0;
  int VIEW_BLINK = 1;
  int VIEW_PINREAD = 2;
  int VIEW_PINWRITE = 3;
  int VIEW_COMMANDS = 4;
}
  • líneas 25, 28, 31, 40: configuración de la capa [DAO]. Esta aplicación consulta un servidor web/JSON;
  • línea 37: esta aplicación tiene pestañas;
  • línea 43: esta aplicación tiene cinco fragmentos;
  • líneas 46-50: los números de los cinco fragmentos;
  • línea 34: adyacencia de fragmentos. El desarrollador puede establecer aquí un valor dentro del rango [1, FRAGMENTS_COUNT-1];

5.6.7.2. La clase [CoreState]

La clase [CoreState] es la clase padre de los estados de fragmento:


package client.android.architecture.custom;
 
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // state of the fragment's menu (if any)
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • líneas 12-16: aquí deben declararse las clases de estado para los cinco fragmentos;

5.6.8. La clase [MainActivity]

  

La clase [MainActivity] será la siguiente:


package client.android.activity;
 
import android.support.design.widget.TabLayout;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.ArduinoCommand;
import client.android.dao.entities.ArduinoResponse;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.*;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
 
import java.util.List;
import java.util.Locale;
 
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // [DAO] layer
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // parent class methods -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // create the five tabs
    for (int i = 0; i < 5; i++) {
      TabLayout.Tab newTab = tabLayout.newTab();
      newTab.setText(getFragmentTitle(i));
      tabLayout.addTab(newTab);
    }
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
  }
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    Locale l = Locale.getDefault();
    switch (position) {
      case 0:
        return getString(R.string.config_title).toUpperCase(l);
      case 1:
        return getString(R.string.blink_title).toUpperCase(l);
      case 2:
        return getString(R.string.pinread_title).toUpperCase(l);
      case 3:
        return getString(R.string.pinwrite_title).toUpperCase(l);
      case 4:
        return getString(R.string.commands_title).toUpperCase(l);
    }
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // display fragment position
    navigateToView(position, ISession.Action.NAVIGATION);
  }
 
  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }
 
  // IDao implementation -----------------------------------------
}
  • líneas 46-50: creación de las cinco pestañas de la aplicación;
  • línea 48: los títulos de las pestañas son proporcionados por el método de las líneas 63-79;
  • los cinco fragmentos se instancian en la línea 60. Debido a las anotaciones AA, las clases de fragmentos son las presentadas anteriormente, sufijadas con un guión bajo;
  • líneas 63-79: se define un título para cada fragmento. Estos títulos se recuperarán del archivo [res/values/strings.xml]
  

El contenido de [strings.xml] es el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<resources>
 
  <!-- application name -->
  <string name="app_name">[arduinos-client-01]</string>
  <!-- Fragments and tabs -->
  <string name="config_title">[Config]</string>
  <string name="blink_title">[Blink]</string>
  <string name="pinread_title">[PinRead]</string>
  <string name="pinwrite_title">[PinWrite]</string>
  <string name="commands_title">[Commands]</string>
 
</resources>

Tarea: Cree los elementos enumerados anteriormente y compile el proyecto. No debería haber errores.


Ejecute el proyecto. Deberías ver la siguiente vista en el emulador:

Image

Examine los registros que acompañan a la visualización de la primera vista y siga los distintos pasos que se han ejecutado. Cambia de pestaña y continúa siguiendo los registros.

5.6.9. La vista [config] XML

La vista [config] XML tendrá el siguiente aspecto:

La vista anterior es generada por el siguiente código XML:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
 
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <TextView
      android:id="@+id/txt_TitreConfig"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentTop="true"
      android:layout_centerHorizontal="true"
      android:layout_marginTop="150dp"
      android:text="@string/txt_TitreConfig"
      android:textSize="@dimen/title"/>
 
    <TextView
      android:id="@+id/txt_UrlServiceRest"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_TitreConfig"
      android:layout_marginTop="50dp"
      android:text="@string/txt_UrlServiceRest"
      android:textSize="20sp"/>
 
    <EditText
      android:id="@+id/edt_UrlServiceRest"
      android:layout_width="300dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_UrlServiceRest"
      android:layout_alignBottom="@+id/txt_UrlServiceRest"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_UrlServiceRest"
      android:ems="10"
      android:hint="@string/hint_UrlServiceRest"
      android:inputType="textUri">
 
      <requestFocus/>
    </EditText>
 
    <TextView
      android:id="@+id/txt_MsgErreurIpPort"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_UrlServiceRest"
      android:layout_marginTop="20dp"
      android:text="@string/txt_MsgErreurUrlServiceRest"
      android:textColor="@color/red"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_MsgErreurIpPort"
      android:layout_marginTop="40dp"
      android:text="@string/title_arduinos_list"
      android:textColor="@color/blue"
      android:textSize="20sp"/>
 
    <Button
      android:id="@+id/btn_Refresh"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_refresh"/>
 
    <Button
      android:id="@+id/btn_Cancel"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_cancel"
      android:visibility="invisible"/>
 
    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>
 
  </RelativeLayout>
</ScrollView>

La vista utiliza cadenas (android:texto en las líneas 15, 25, 37, 50, 61, 73) que se definen en el archivo [res/values/strings]:

  

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <string name="app_name">android-home-automation</string>
 
    <!-- Fragments and tabs -->
    <string name="config_title">[Config]</string>
    <string name="blink_title">[Blink]</string>
    <string name="pinread_title">[PinRead]</string>
    <string name="pinwrite_title">[PinWrite]</string>
    <string name="commands_title">[Commands]</string>
 
    <!-- Config -->
    <string name="txt_ConfigTitle">Connect to the server</string>
    <string name="txt_UrlServiceRest">Web service URL / JSON</string>
    <string name="txt_MsgErreurUrlServiceRest">The service URL must be entered in the format IP1.IP2.IP3.IP4:Port/context</string>
    <string name="hint_UrlServiceRest">e.g. (192.168.1.120:8080/rest)</string>
    <string name="btn_cancel">Cancel</string>
    <string name="btn_refresh">Refresh</string>
    <string name="title_list_arduinos">List of Connected Arduinos</string>
 
</resources>

La vista utiliza colores (android:textColor en las líneas 51 y 62) definidos en el fichero [res/values/colors]:

  

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
  <color name="floral_white">#FFFAF0</color>
  <!-- app -->
  <color name="red">#FF0000</color>
  <color name="blue">#0000FF</color>
  <color name="wheat">#FFEFD5</color>
</resources>

La vista utiliza dimensiones (android:textSize en la línea 16) que se definen en el archivo [res/values/dimens]:

  

<resources>
  <!-- Default screen margins, per the Android Design guidelines. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="fab_margin">16dp</dimen>
  <dimen name="appbar_padding_top">8dp</dimen>
  <!-- app -->
  <dimen name="title">30dp</dimen>
</resources>

Esta técnica no se ha utilizado para todas las dimensiones. Sin embargo, es el método recomendado. Permite cambiar las dimensiones en un único lugar.


Tarea: Crea los elementos anteriores.


Vuelva a ejecutar el proyecto. Debería ver la siguiente vista:

Image

5.6.10. El fragmento [ConfigFragment]

Para manejar la nueva vista [config], el código del fragmento [ConfigFragment] cambia como sigue:


package client.android.fragments.behavior;
 
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_empty)
public class ConfigFragment extends AbstractFragment {
 
  // UI elements
  @ViewById(R.id.btn_Refresh)
  protected Button btnRefresh;
  @ViewById(R.id.btn_Cancel)
  protected Button btnCancel;
  @ViewById(R.id.edt_UrlServiceRest)
  protected EditText edtUrlServiceRest;
  @ViewById(R.id.txt_IPPortErrorMessage)
  protected TextView txtRestServiceErrorMessage;
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
 
  @Click(R.id.btn_Refresh)
  protected void refresh() {
  }
 
  // Fragment lifecycle management -------------------------------------
 
  @Override
  public CoreState saveFragment() {
    return new ConfigFragmentState();
  }
 
  @Override
  protected int getNumView() {
    return IMainActivity.VUE_CONFIG;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // First visit?
    if(previousState==null){
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
  }
 
  @Override
  protected void notifyEndOfUpdates() {
    // buttons
    initButtons();
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
  }
 
  // private methods --------------------------------------------
 
  private void initButtons() {
    // The [Run] button replaces the [Cancel] button
    btnCancel.setVisibility(View.INVISIBLE);
    btnRefresh.setVisibility(View.VISIBLE);
  }
}
  • líneas 23-32: los elementos de la interfaz visual;
  • líneas 58-60: en la primera visita al fragmento, el mensaje de error está oculto;
  • líneas 73-76: cada vez que se muestra el fragmento, se oculta el botón [Cancelar] (línea 82) y se muestra el botón [Actualizar] (líneas 86-87). De hecho, en esta aplicación, no se puede mostrar un fragmento mientras está en curso una operación asíncrona y, por lo tanto, el botón [Cancelar] está visible;

Tarea: Crea los elementos anteriores.


Ejecute esta nueva versión. La primera vista debería tener ahora este aspecto:

Image

5.6.10.1. El botón [Actualizar

Por ahora, manejaremos el clic en el botón [Actualizar] de la siguiente manera:


@Click(R.id.btn_Refresh)
  protected void doRefresh() {
    // We're going to start a task—we're preparing to wait
    beginWaiting(1);
  }
 
  @Click(R.id.btn_Cancel)
  protected void doCancel() {
    if (isDebugEnabled) {
      Log.d(className, "Cancellation requested");
    }
    // cancel asynchronous tasks
    cancelRunningTasks();
  }
 
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to wait for tasks
    beginRunningTasks(numberOfRunningTasks);
    // The [Cancel] button replaces the [Refresh] button
    btnRefresh.setVisibility(View.INVISIBLE);
    btnCancel.setVisibility(View.VISIBLE);
}
  // fragment lifecycle management -------------------------------------
...
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // buttons in their initial state
    initButtons();
  }
 
  // private methods --------------------------------------------
 
  private void initButtons() {
    // the [Run] button replaces the [Cancel] button
    btnCancel.setVisibility(View.INVISIBLE);
    btnRefresh.setVisibility(View.VISIBLE);
  }
  • líneas 1-5: el método que se ejecuta cuando se hace clic en el botón [Actualizar];
  • línea 4: comenzamos la espera;
  • línea 18: pasamos el número de tareas asíncronas a lanzar a la clase padre. Aparecerá la imagen de carga;
  • líneas 20-21: esta espera hará que aparezca el botón [Cancelar], desaparezca el botón [Actualizar] y aparezca la imagen de carga. No ocurre nada más. Sin embargo, el usuario puede hacer clic en el botón [Cancelar]. Entonces se ejecutará el método de las líneas 7-14;
  • línea 13: se pide a la clase padre que cancele todas las tareas. La clase lo hará y a su vez llamará al método de las líneas 25-29 para indicar que todas las tareas se han completado. El parámetro [runningTasksHaveBeenCanceled] tendrá el valor verdadero para indicar que las tareas se han cancelado;
  • Líneas 35-36: El botón [Cancelar] desaparecerá, mientras que el botón [Actualizar] reaparecerá.

Tarea: Realice estos cambios y, a continuación, ejecute el proyecto. Compruebe que el botón [Actualizar] inicia la espera y que el botón [Cancelar] la detiene. Observa los registros.


5.6.10.2. Validación de entradas

En la versión anterior, no validábamos el URL introducido. Para validarlo, añadimos el siguiente código en [ConfigFragment]:


// the entered values
  private String urlServiceRest;
 
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // we check the inputs
    if (!pageValid()) {
      return;
    }
    // we're going to start a task - we prepare for the wait
    beginWaiting(1);
  }
 
  // checking the inputs
  private boolean pageValid() {
    // initially, no error message
    txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    // retrieve the server's IP and port
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    // check its validity
    try {
      URI uri = new URI(urlServiceRest);
      String host = uri.getHost();
      int port = uri.getPort();
      if (host == null || port == -1) {
        throw new Exception();
      }
    } catch (Exception ex) {
      // display error message
      txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
      // return to the UI
      return false;
    }
    // OK
    return true;
  }
  • línea 2: el URL introducido;
  • líneas 7-9: antes de hacer nada, comprobamos la validez de la entrada;
  • línea 19: recuperar el URL introducido y añadirle el prefijo [http://];
  • línea 22: intentamos construir un objeto URI (Uniform Resource Identifier) con él. Si el URL introducido es sintácticamente incorrecto, se lanzará una excepción;
  • líneas 23-27: se lanza una excepción si el URI es válido pero [host==null] y [port==-1]. Este es un escenario posible;
  • línea 30: se ha producido una excepción. Se muestra el mensaje de error;
  • línea 32: devolvemos [false] para indicar que la página no es válida;
  • línea 35: no se ha producido ningún error. Devolvemos [true] para indicar que la página es válida;

Asignación: Implementar la funcionalidad anterior.


Pruebe esta nueva versión y compruebe que los URLs no válidos se marcan correctamente.

5.6.10.3. Visualización de la lista de Arduinos

  

Las diferentes vistas tendrán que mostrar la lista de Arduinos conectados. Para ello, definiremos diferentes clases y una vista XML:

  • Un Arduino estará representado por la clase [Arduino] [1];
  • la clase [CheckedArduino] [1] hereda de la clase [Arduino], a la que hemos añadido un booleano para indicar si el Arduino ha sido seleccionado en una lista;

La clase [Arduino] es la que ya utiliza el servidor y que se presenta en la sección 5.4.2. Es el siguiente:


package android.arduinos.entities;
 
import java.io.Serializable;
 
public class Arduino implements Serializable {
  // data
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;
 
 // getters and setters
...
}
  • línea 7: [id] es el identificador del Arduino;
  • línea 8: su descripción;
  • línea 9: su dirección MAC;
  • línea 10: su dirección IP;
  • línea 11: el puerto en el que escucha comandos;

Esta clase corresponde a la cadena JSON recibida del servidor al solicitar la lista de Arduinos conectados:

La clase [CheckedArduino] hereda de la clase [Arduino]:


package android.arduinos.entities;
 
public class CheckedArduino extends Arduino {
    private static final long serialVersionUID = 1L;
    // an Arduino can be selected
    private boolean isChecked;
 
    // constructor
    public CheckedArduino(Arduino arduino, boolean isChecked) {
        // parent
        super(arduino.getId(), arduino.getDescription(), arduino.getMac(), arduino.getIp(), arduino.getPort());
        // local
        this.isChecked = isChecked;
    }
 
    // getters and setters
    public boolean isChecked() {
        return isChecked;
    }
 
    public void setChecked(boolean isChecked) {
        this.isChecked = isChecked;
    }
 
}
  • Línea 3: La clase [CheckedArduino] hereda de la clase [Arduino];
  • línea 6: añadimos una variable booleana que nos dirá si se ha seleccionado un Arduino de la lista de Arduinos mostrada;

En [ConfigFragment], simularemos la recuperación de la lista de Arduinos conectados.

  

  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
..
  @Click(R.id.btn_Rafraichir)
  protected void doRefresh() {
    // check the input
    if (!pageValid()) {
      return;
    }
    // we're going to start a task - we prepare for the wait
    beginWaiting(1);
    // clear the list of Arduinos
    clearArduinos();
    // we request the list of Arduinos in the background
    getArduinosInBackground();
  }
 
  private void getArduinosInBackground() {
   ...
  }
 
  // Clear the list of Arduinos
  private void clearArduinos() {
    // create an empty list
    List<String> strings = new ArrayList<>();
    // display it
    listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
  • línea 2: el ListView que muestra los Arduinos conectados al servidor;
  • línea 5: el método que recupera la lista de Arduinos conectados;
  • línea 11: le decimos a la clase padre que vamos a lanzar una tarea asíncrona;
  • línea 12: borramos la lista de Arduinos mostrada actualmente;
  • línea 15: solicitamos la lista de Arduinos conectados como tarea en segundo plano;
  • líneas 23-28: el método que borra la lista de Arduinos mostrada actualmente;

El [getArduinosInBackgroundel método [...] es el siguiente:


  private void getArduinosInBackground() {
    // create a dummy list of Arduinos
    List<Arduino> arduinos = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
      arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
    }
    // Simulate a server response
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // cancel the wait
    cancelWaitingTasks();
    // update the buttons
    initButtons();
    // process the response
    consumeArduinoResponse(response);
}
  • líneas 3-6: crear una lista de 20 Arduinos;
  • líneas 8-9: construimos la respuesta de tipo [Response<List<Arduino>>] (sección 5.4.2) que encapsulará la lista de Arduinos creada;
  • línea 11: cancelar la espera;
  • línea 13: restablecer el estado inicial de los botones;
  • línea 15: consumir la respuesta;

El método [consumeArduinosResponse] es el siguiente:


  // display response
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // error?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // return to UI
      return;
    }
    // create a list of [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // display them
    showArduinos(checkedArduinos);
}
  • líneas 4-11: comprueba el código de error en la respuesta enviada por el servidor:
  • línea 4: si el código de error no es cero;
  • línea 6: mostrar los mensajes almacenados por el servidor en el campo [mensajes] de la respuesta;
  • línea 8: volver al UI;
  • líneas 11-16: si no hubo errores, mostrar la lista de Arduinos recibidos, después de convertirla a un Lista<CheckedArduino> tipo;

El [showArduinosel método [...] es el siguiente:


  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // create a list of Strings from the list of Arduinos
    List<String> strings = new ArrayList<>();
    for (CheckedArduino checkedArduino : checkedArduinos) {
      strings.add(checkedArduino.toString());
    }
    // display it
    listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}

Tarea: Realice los cambios anteriores y ejecute su proyecto.


Debería ver la siguiente vista cuando haga clic en el botón [Actualizar]:

Image

La entrada en [1] no se utiliza. Por lo tanto, puede introducir cualquier cosa siempre que siga el formato esperado.

5.6.10.4. Una plantilla para mostrar un Arduino

Actualmente, los Arduinos conectados se muestran en la vista [Config] de la siguiente manera:

Image

Ahora queremos mostrarlos de la siguiente manera:

Image

  • en [1], una casilla de verificación que le permitirá seleccionar un Arduino. Esta casilla se ocultará cuando desee mostrar una lista de Arduinos no seleccionables;
  • en [2], el ID de Arduino;
  • en [3], su descripción;

Lo que sigue se basa en los conceptos desarrollados en los proyectos [ejemplo-19] y [ejemplo-19B] de la sección 1.20. Revíselas si es necesario.

En primer lugar, creamos la vista que mostrará un elemento de la lista de Arduinos:

 

El código de la vista [listarduinos_item] anterior es el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat"
    android:orientation="vertical" >
 
    <CheckBox
        android:id="@+id/checkBoxArduino"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/txt_arduino_description" />
 
    <TextView
        android:id="@+id/TextView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_marginLeft="40dp"
        android:text="@string/txt_arduino_id" />
 
    <TextView
        android:id="@+id/txt_arduino_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/TextView1"
        android:text="@string/dummy"
        android:textColor="@color/blue" />
 
    <TextView
        android:id="@+id/TextView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_arduino_id"
        android:text="@string/txt_arduino_description" />
 
    <TextView
        android:id="@+id/txt_arduino_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignTop="@+id/TextView2"
        android:layout_toRightOf="@+id/TextView2"
        android:text="@string/dummy"
        android:textColor="@color/blue" />
 
</RelativeLayout>
  • líneas 9-15: la casilla de verificación;
  • líneas 17-23: el texto [Id: ];
  • líneas 25-33: aquí se introducirá el Arduino ID;
  • líneas 35-43: el texto [Descripción: ];
  • líneas 45-53: aquí se introducirá la descripción del Arduino;

Esta vista utiliza texto (líneas 23, 32, 43) definido en [res/values/strings.xml]:


    <string name="dummy">XXXXX</string>
 
    <!--  listarduinos_item -->
    <string name="txt_arduino_id">ID: </string>
<string name="txt_arduino_description">Description: </string>

La vista también utiliza un color (líneas 33, 53) definido en [res / values / colors.xml]:


<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <color name="red">#FF0000</color>
    <color name="blue">#0000FF</color>
    <color name="wheat">#FFEFD5</color>
    <color name="floral_white">#FFFAF0</color>
 
</resources>

El gestor de vistas de un elemento de la lista Arduino

  

La clase [ListArduinosAdapter] es la clase llamada por el [ListView] para mostrar cada elemento en la lista de Arduino. Su código es el siguiente:


package istia.st.android.vues;
 
import istia.st.android.R;
...
 
public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {
 
    // the array of Arduinos
    private List<CheckedArduino> arduinos;
    // the runtime context
    private Context context;
    // the layout ID for a row in the Arduino list
    private int layoutResourceId;
    // whether the row contains a checkbox
    private Boolean selectable;
 
    // constructor
    public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
        // parent
        super(context, layoutResourceId, arduinos);
        // store the data
        this.arduinos = arduinos;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.selectable = selectable;
    }
 
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • línea 18: el constructor de la clase toma cuatro parámetros: la actividad actualmente en ejecución, el ID de la vista a mostrar para cada elemento de la fuente de datos, la fuente de datos que rellena la lista, y un booleano que indica si la casilla de verificación asociada a cada Arduino debe mostrarse o no;
  • Líneas 8-15: Estos cuatro datos se almacenan localmente;

Línea 29: El método [getView] es responsable de generar la vista #[position] en el [ListView] y manejar sus eventos. Su código es el siguiente:


@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // the current Arduino
        final CheckedArduino arduino = arduinos.get(position);
        // create the current line
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // retrieve references to the [TextView]
        TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
        TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
        // populate the row
        txtArduinoId.setText(arduino.getId());
        txtArduinoDesc.setText(arduino.getDescription());
        // The CheckBox is not always visible
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // Set its value
            ck.setChecked(arduino.isChecked());
            // handle the click
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
        }
        // return the row
        return row;
    }
  • línea 2: el primer parámetro es la posición en el [ListView] de la línea a crear. También es la posición en la lista de Arduinos almacenada localmente;
  • línea 4: recuperamos una referencia al Arduino que se asociará a la fila construida;
  • línea 6: la fila actual se construye a partir de la vista [listarduinos_item.xml];
  • líneas 8-9: se recuperan las referencias a los dos [TextView]s;
  • líneas 11-12: se asignan valores a los dos [TextView]s;
  • línea 14: se recupera una referencia a la casilla de verificación;
  • línea 15: se hace visible o no, dependiendo del valor [seleccionable] pasado inicialmente al constructor;
  • línea 16: si la casilla de verificación está presente;
  • línea 18: se le asigna el valor [isChecked] del Arduino actual;
  • líneas 20-26: manejamos el clic en la casilla de verificación;
  • línea 23: el valor de la casilla de verificación se almacena en el Arduino actual;

Gestión de la lista de Arduinos

La visualización de la lista Arduino es manejada actualmente por dos métodos de la clase [ConfigFragment]:

  • [clearArduinos]: que muestra una lista vacía;
  • [showArduinos]: que muestra la lista devuelta por el servidor;

Estos dos métodos funcionan de la siguiente manera:


  // Clear the list of Arduinos
  private void clearArduinos() {
    // Display an empty list
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
 
  // Display list of Arduinos
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // display the Arduinos
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
}

Tarea: Realiza estos cambios y prueba la nueva aplicación.


Image

5.6.10.5. La sesión

La sesión es donde almacenamos la información compartida entre los fragmentos y la actividad. Todos los fragmentos necesitan mostrar la lista de Arduinos conectados. Así, una versión inicial de la sesión tendría este aspecto:


package client.android.architecture.custom;
 
import client.android.activity.CheckedArduino;
import client.android.architecture.core.AbstractSession;
 
import java.util.ArrayList;
import java.util.List;
 
public class Session extends AbstractSession {
  // data to be shared between fragments themselves and between fragments and the activity
  // Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
  // Don't forget the getters and setters needed for JSON serialization/deserialization
 
  // the list of Arduinos
  private List<CheckedArduino> checkedArduinos = new ArrayList<>();
 
  // getters and setters
...
}

Tarea: Cree la clase [Session] mostrada arriba.


La creación de esta sesión requiere que modifiquemos el código existente de la siguiente manera:


  // display response
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // error?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // cancel
      cancel();
      // return to UI
      return;
    }
    // create a list of [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // Set it in the session
    session.setCheckedArduinos(checkedArduinos);
    // display them
    showArduinos(checkedArduinos);
    // cancel the wait
    cancelWaitingTasks();
}
  • línea 18: se coloca en la sesión la lista de Arduinos creada por las líneas anteriores;

5.6.10.6. Gestión del estado de los fragmentos

Cuando se gira el dispositivo, los componentes visuales de la vista se muestran (por defecto) en el estado en que estaban cuando se diseñó la vista:

  • el [ListView] contiene los elementos que el diseñador colocó allí;
  • el mensaje de error está en el estado visible o no visible en el que lo colocó el diseñador;

Los estados de los componentes visuales en tiempo de diseño pueden ser apropiados o no a la hora de restaurar un fragmento. ¿Cuál es el caso aquí?

  • el [ListView] debe mostrar la lista de Arduinos conectados. Por lo tanto, no se puede utilizar el valor de [ListView] en tiempo de diseño;
  • El [TextView] del mensaje de error debe restaurarse al estado visible u oculto que tenía en el momento de guardar. Su valor en tiempo de diseño puede no ser adecuado para estos dos casos;

Por lo tanto, debemos guardar el estado de estos dos componentes al guardar el estado del fragmento:

  • la lista de Arduinos conectados;
  • la visibilidad (mostrada/oculta) del mensaje de error al entrar en el servicio web URL/JSON;

Como la lista de Arduinos está presente en la sesión, se guardará automáticamente. La visibilidad del mensaje de error se guardará en la siguiente clase [ConfigFragmentState]:

  

package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class ConfigFragmentState extends CoreState {
 
  // error message visibility
  private boolean txtMsgErrorUrlServiceRestVisible;
 
  // getters and setters
...
}

Tarea: Crea la clase [ConfigFragmentState] mostrada arriba.


Para restablecer correctamente los estados de los fragmentos, es necesario modificar sus métodos [getNumView] y [saveFragment]. Por ejemplo, el del fragmento [BlinkFragment] es actualmente el siguiente:


  @Override
  public CoreState saveFragment() {
    // the fragment must be saved
    DummyFragmentState state = new DummyFragmentState();
    // ...
    return state;
    // if there is nothing to save, use [return new CoreState();] and remove the [DummyFragmentState] class
  }
 
  @Override
  protected int getNumView() {
    // Return the fragment number in the array of fragments managed by the activity (see MainActivity)
    return 0;
}

Si no se hace nada, el estado representado en la línea 6 se guardará en el elemento 0 (línea 13) del archivo CoreState[] coreStates de la clase [AbstractSession] (línea 5 más abajo):


public class AbstractSession implements ISession {
  ...
 
  // view state
  private CoreState[] coreStates = new CoreState[0];
...

Sin embargo, debe guardarse en el elemento correspondiente al fragmento ID [BlinkFragment] en la matriz de fragmentos definida en la clase [MainActivity] (línea 9 más abajo):


@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  ...
 
  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
  }
 
 

El fragmento IDs se ha definido en la interfaz [IMainActivity]:


public interface IMainActivity extends IDao {
 
  ...
 
  // view numbers
  int VIEW_CONFIG = 0;
  int VIEW_BLINK = 1;
  int VIEW_PINREAD = 2;
  int VIEW_PINWRITE = 3;
  int VIEW_COMMANDS = 4;
}

En definitiva, el estado del fragmento [BlinkFragment] se gestionará correctamente si escribimos:


  @Override
  public CoreState saveFragment() {
    // the fragment must be saved
    DummyFragmentState state = new DummyFragmentState();
    // ...
    return state;
    // if there is nothing to save, use [return new CoreState();] and remove the [DummyFragmentState] class
  }
 
  @Override
  protected int getNumView() {
    // Return the fragment ID in the array of fragments managed by the activity (see MainActivity)
    return IMainActivity.VUE_BLINK;
}
  • Línea 14: Devuelve el fragmento ID [BlinkFragment] en el array de fragmentos gestionados por la actividad;

Además, la clase [CoreState], que es el padre de los estados de fragmento, es actualmente la siguiente (véase la sección 5.6.7.2):


package client.android.architecture.custom;
 
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // state of the fragment's menu (if any)
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
....
}
  • Líneas 12-16: La clase [DummyFragmentState] no aparece entre las clases hijas de la clase [CoreState]. Sin embargo, el método [saveFragment] de la clase [BlinkFragment] devuelve actualmente un valor tipo [DummyFragmentState]. Si se deja como está, la serialización/deserialización de la sesión fallará y la sesión no se restaurará, provocando un fallo de la aplicación;

El método [saveFragment] del fragmento [BlinkFragment] debe reescribirse como sigue:


  @Override
  public CoreState saveFragment() {
    // the fragment must be saved
    BlinkFragmentState state = new BlinkFragmentState();
    // ...
    return state;
    // if there is nothing to save, do [return new CoreState();] and remove the [DummyFragmentState] class
}

Tarea: En cada fragmento, modifica el método [getNumView] para que devuelva el número de fragmento, y el método [saveFragment] para que devuelva una instancia de la clase de estado del fragmento (como se muestra arriba).


5.6.10.7. Gestión del ciclo de vida de los fragmentos

Aquí nos centramos en el ciclo de vida del fragmento [ConfigFragment], concretamente en los cuatro métodos:

  • [saveFragment]: debe guardar el estado del fragmento para poder restaurarlo más tarde;
  • [initFragment]: que debe inicializar ciertos campos del fragmento si es necesario. Este método se llama cuando se inicia la aplicación y cada vez que se gira el dispositivo. Más concretamente, se llama cuando el fragmento se hace visible tras uno de los dos eventos anteriores;
  • [initView]: que debe inicializar ciertos componentes de la vista si es necesario. Se llama a este método cada vez que se ha llamado a [initFragment] y cuando hay que redibujar la vista porque el fragmento se ha movido, en algún momento, fuera de las proximidades del fragmento visualizado. Como antes, se llama cuando el fragmento se hace visible después de uno de estos eventos;
  • [updateOnRestore]: que se ejecuta después de los dos métodos anteriores cuando se ha girado el dispositivo, pero también cuando se ha producido la navegación. Su función es restablecer el estado anterior del fragmento;

Estos métodos serán los siguientes:


// Arduino list adapter
  private ListArduinosAdapter adapterListArduinos;
 
...
  // fragment lifecycle management -------------------------------------
 
  @Override
  public CoreState saveFragment() {
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtMsgErreurUrlServiceRestVisible(txtMsgErreurUrlServiceRest.getVisibility() == View.VISIBLE);
    return state;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // listArduinos adapter
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // Link listview to adapter
    listArduinos.setAdapter(adapterListArduinos);
    // First visit?
    if (previousState == null) {
      // Empty ListView - created by [initFragment]
      // hide error message
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    } else {
      // Restore visibility of the error message
      ConfigFragmentState state = (ConfigFragmentState) previousState;
      txtMsgErreurUrlServiceRest.setVisibility(state.isTxtMsgErreurUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
    }
  }
 
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
  }
 
 
  @Override
  protected void notifyEndOfUpdates() {
    // buttons
    initButtons();
}
  • línea 2: el ListView para los Arduinos. Es una variable global porque se utiliza en diferentes métodos;
  • líneas 7-12: el método [saveFragment] guarda la visibilidad de el TextView txtMsgErreurUrlServiceRestVisible (línea 10) a un tipo [ConfigFragmentState];
  • líneas 14-19: el método [initFragment] inicializa el adaptador de la línea 2 con la lista de Arduinos actualmente en la sesión (línea 17). Ten en cuenta que el papel de [initFragment] es inicializar los campos del fragmento. Aquí, esta inicialización debe realizarse en todos los casos, tanto si se trata de la primera visita (previousState == null) o no;
  • línea 17: vemos que el adaptador está vinculado a la fuente de datos [session.getCheckedArduinos]. Este no debe tener el valor null. Por esta razón, el campo [session.checkedArduinos] se inicializa con una lista vacía en la sesión:

  // the list of Arduinos
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
  • líneas 21-35: el método [initView] se encarga de inicializar ciertos componentes de la interfaz visual, en particular aquellos cuyos valores no se conservan al rotar el dispositivo;
  • línea 24: el Arduino ListView está vinculado al adaptador de la línea 2;
  • líneas 28-32: la primera visita se distingue de las demás;
  • línea 29: en la primera visita, debe mostrarse un [ListView] vacío. Esto es así porque, en la primera visita, el adaptador [ListView] estaba asociado a una lista vacía (línea 17);
  • línea 31: el mensaje de error está oculto;
  • líneas 32-36: el caso de que no sea la primera visita;
  • el [ListView] ya está en el estado correcto desde la línea 24. No hay nada más que hacer;
  • líneas 34-35: el mensaje de error se restaura al estado en que se encontraba la última vez que se guardó el fragmento;
  • líneas 31-36: el método [updateOnRestore] debe devolver el fragmento a su estado inicial. Llegamos al método [updateOnRestore] de dos maneras:
    • ya sea porque se ha girado el dispositivo. En este caso, todas las inicializaciones necesarias ya se han realizado en [initView];
    • ya sea porque estamos navegando desde una pestaña a la pestaña [Config]. Si el fragmento [Config] ha salido de las inmediaciones de los fragmentos mostrados desde que lo abandonamos, el método [initView] ya se ha ejecutado y el fragmento ya se encuentra en el estado deseado. Si el fragmento [Config] no ha abandonado la lista de fragmentos visualizados desde que salimos de ella, sus componentes visuales no han cambiado de estado y no hay nada que hacer;

Vemos que el método [updateOnRestore] no tiene nada que hacer. A veces es así, a veces no. La diferencia viene del método [updateOnSubmit]: si este método hace algo que hace innecesarias ciertas inicializaciones hechas en [initView], entonces esas inicializaciones deberían hacerse en el método [updateOnRestore]. Tomemos el ejemplo de un botón de radio con tres valores: V1, V2 y V3. Quizás en el caso de una navegación asociada a una acción [SUBMIT], el botón de opción seleccionado debe ser siempre el que tiene el valor V1. En este caso, restablecer el valor del botón de opción en el método [initView] es innecesario, ya que en el caso de una acción [SUBMIT], este valor será sustituido por el proporcionado por el método [updateOnSubmit]. Por tanto, es preferible trasladar esta restauración al método [updateOnRestore] para evitar realizar una operación innecesaria.

  • líneas 48-52: el método [notifyEndOfUpdates] se ejecuta después de todos los anteriores;
  • Línea 51: Los botones vuelven a su estado inicial: se muestra el botón [Actualizar] y se oculta el botón [Cancelar]:

Tarea: Añade el código anterior a [ConfigFragment] y ejecuta la aplicación. Observa que cuando giras el dispositivo, la pestaña [Config] mantiene su estado (mensaje de error, lista de Arduinos). Comprueba que el mismo comportamiento se produce cuando simplemente navegas desde la pestaña [Config] a la pestaña [Comandos] --> pestaña [Config]. En este último caso, si ha establecido la adyacencia de fragmentos en 1 en [IMainActivity], la vista [ConfigFragment] se destruye al cambiar a la pestaña [Comandos] y se vuelve a crear al volver a la pestaña [Config]. Durante las pruebas, examine los registros.


5.6.10.8. Mejora del código

El código del fragmento [ConfigFragment] puede mejorarse. Por ejemplo, escribimos:


// Arduino list adapter
  private ListArduinosAdapter adapterListArduinos;
 
...
 
  // display Arduino list
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // display the Arduinos
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
  }
 
  // Clear the list of Arduinos
  private void clearArduinos() {
    // display an empty list
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
  • Podemos ver que en las líneas 9 y 16, estamos utilizando una variable local que está desconectada del campo de la línea 2, a pesar de que estamos tratando de manipular la misma entidad;

Actualizamos el código como sigue:


  // adapter for the list of Arduinos
  private ListArduinosAdapter adapterListArduinos;
 
  @Click(R.id.btn_Rafraichir)
  protected void doRefresh() {
  ...
  }
 
  private void getArduinosInBackground() {
 ...
    // process it
    consumeArduinosResponse(response);
  }
 
  // display response
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // error?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // cancel
      doCancel();
      // return to UI
      return;
    }
    // create a list of [CheckedArduino]
    List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
    checkedArduinos.clear();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // display them
    adapterListArduinos.notifyDataSetChanged();
    // Cancel the wait
    cancelWaitingTasks();
}
 
  @Override
  protected void initFragment(CoreState previousState) {
    // listArduinos adapter
    listArduinosAdapter = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // bind listview / adapter
    listArduinos.setAdapter(adapterListArduinos);
    ...
}
  • Cuando se ejecuta el método de la línea 5, el ciclo de vida del fragmento se ha completado. Por lo tanto:
    • el adaptador de la línea 2 se ha asociado a su fuente de datos (línea 41);
    • el [ListView] de los Arduinos conectados se ha vinculado a este adaptador (línea 48);

Cuando queremos cambiar la visualización del [ListView], tenemos que hacer dos cosas:

  • cambiar el contenido de la fuente de datos [session.checkedArduinos];
  • notificar al adaptador este cambio mediante la instrucción [adapterListArduinos.notifyDataSetChanged()];

Es importante cambiar el contenido de la fuente de datos, no la fuente de datos en sí. Si cambiamos la fuente de datos en sí, la operación [adapterListArduinos.notifyDataSetChanged()] seguirá mostrando la fuente de datos antigua. Tendríamos entonces que asociar el adaptador con la nueva fuente de datos.

El código es el siguiente:

  • línea 27: recuperamos la fuente de datos;
  • línea 28: lo borramos. Por esta razón, hemos eliminado el método [clearArduinos];
  • líneas 29-31: añadimos nuevos elementos a esta lista ahora vacía;
  • línea 33: le decimos al adaptador que refresque. Esto refrescará la pantalla del [ListView] asociado;

Tarea: Realice estos cambios y compruebe que su aplicación sigue funcionando.


5.6.11. Comunicación entre puntos de vista

Para verificar la comunicación entre vistas, haremos que todas las demás vistas muestren la lista de Arduinos obtenida por la vista [Config]. Empecemos por la vista [blink.xml]. Mientras que antes no mostraba nada, ahora mostrará la lista de Arduinos conectados:

Image

 

El código XML para la vista [blink.xml] será el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
 
  <?xml version="1.0" encoding="utf-8"?>
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:id="@+id/RelativeLayout1"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
 
    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_marginTop="150dp"
      android:text="@string/arduinos_list_title"
      android:textColor="@color/blue"
      android:textSize="20sp" />
 
    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>
 
  </RelativeLayout>
</ScrollView>

Este código fue tomado directamente de la vista [config.xml]. Simplemente modificamos el margen superior en la línea 19.


Tarea: Duplique este código en las vistas [commands.xml, pinread.xml, pinwrite.xml].


También cambia el código del fragmento [BlinkFragment] asociado a la vista [blink.xml]:

  

  // visual components
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
 
  // Arduino list adapter
  private ListArduinosAdapter adapterListArduinos;
...
 
  // methods required by the parent class -------------------------------------------------------
 
...
  @Override
  protected void initFragment(CoreState previousState) {
    // listArduinos adapter
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), true);
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // Link listview / adapter
    listArduinos.setAdapter(adapterListArduinos);
  }
...
  • líneas 2-3: el componente [ListView] para los Arduinos conectados;
  • línea 6: el adaptador para este [ListView];
  • líneas 12-23: el código de los métodos [initFragment] y [initView] es el mismo que el ya utilizado para el fragmento [ConfigFragment];
  • línea 15: cuando el fragmento necesita ser reiniciado, reiniciamos el adaptador de la línea 2 asociándolo a la lista de Arduinos almacenada en la sesión. El último parámetro [true] del constructor [ListArduinosAdapter] significa que queremos ver una casilla de verificación junto a cada Arduino;
  • línea 22: cuando la vista del fragmento necesita ser reiniciada, enlazamos el [ListView] de los Arduinos conectados al adaptador de la línea 6;

Tarea: Duplique este código en los otros fragmentos [CommandsFragment, PinReadFragment, PinWriteFragment]. Ejecute la aplicación y verifique que cada pestaña muestra ahora la lista de Arduino conectados . Compruebe también que si marca los Arduinos en una pestaña y navega a otra pestaña, permanecen marcados en esta última.


Nota: La razón por la que los Arduinos permanecen controlados es la siguiente. La clase [ListArduinosAdapter] se introdujo en la Sección 5.6.10.4. El código relacionado con la casilla de verificación es el siguiente:


        // the current Arduino
        final CheckedArduino arduino = arduinos.get(position);
...
        // the CheckBox is not always visible
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // set its value
            ck.setChecked(arduino.isChecked());
            // handle the click
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
}
  • Líneas 11-15: Si se marca una casilla de verificación en la pestaña X, la propiedad [checked] del Arduino en la fila 2 se establece en verdadero (línea 14);
  • al cambiar a la pestaña Y, se muestra el [ListView] de los Arduinos en esa pestaña. En la línea 9, vemos que si el Arduino de la fila 2 tiene su propiedad [checked] establecida a verdadero, se marcará la casilla [ck] de la fila 5;

5.6.12. La capa [DAO]

Nota: Para esta sección, revise la implementación de la capa [DAO] en el proyecto [example-16B] (consulte la sección 2.8.3).

Hasta ahora, hemos generado manualmente la lista de Arduinos conectados. Ahora la solicitaremos al servidor web / jSON. Para ello, vamos a construir la capa [DAO]:

  

5.6.12.1. La interfaz IDao

La interfaz [IDao] de la capa [DAO] será la siguiente:


package client.android.dao.service;
 
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import rx.Observable;
 
import java.util.List;
 
public interface IDao {
  // Web service URL
  void setWebServiceJsonUrl(String url);
 
  // User
  void setUser(String user, String password);
 
  // Client timeout
  void setTimeout(int timeout);
 
  // Basic authentication
  void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // specific ----------------------------------------
  // list of Arduinos
  Observable<Response<List<Arduino>>> getArduinos();
}
  • líneas 11-26: estas líneas ya están presentes en la interfaz [IDao] del proyecto de plantilla [client-android-skel];
  • línea 30: el método [getArduinos] devuelve la lista de Arduinos conectados como un observable de tipo Observable<[Response<List<Arduino><>>] ;

Tenga en cuenta que [Response<T>] es el tipo de todas las respuestas enviadas por el servidor en forma de cadena JSON:


package client.android.dao.entities;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // response body
    private T body;
 
    // constructors
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

5.6.12.2. La interfaz [WebClient]

  

La interfaz [WebClient] es una interfaz para la que la biblioteca AA proporciona una implementación. Esta interfaz será la siguiente:


package client.android.dao.service;
 
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
 
import java.util.List;
 
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
 
  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);
 
  // specific --------------------------------------
  // list of Arduinos
  @Get("/arduinos")
  Response<List<Arduino>> getArduinos();
}
  • líneas 15-19: estas líneas se incluyen por defecto en la interfaz [WebClient] del proyecto de plantilla [client-android-skel];
  • línea 23: el servidor URL utilizado para recuperar la lista de Arduinos a través de una petición GET. Tenga en cuenta que este URL es relativo a la raíz URL [RestClientRootUrl] en la línea 16;
  • línea 24: el servidor devuelve una cadena JSON de tipo [Response<List<Arduino>>]. Esta cadena JSON se deserializa automáticamente al tipo [Response<List<Arduino>>] utilizando el conversor JSON [MappingJackson2HttpMessageConverter] de la línea 15;

5.6.12.3. La clase [Dao]

La clase [Dao] implementa la interfaz [IDao] de la siguiente manera:


package client.android.dao.service;
 
import android.util.Log;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
 
  // Web service client
  @RestService
  protected WebClient webClient;
  // security
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // the RestTemplate
  private RestTemplate restTemplate;
  // RestTemplate factory
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // create the RestTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // Set the JSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // Set the RestTemplate for the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // Set the web service URL
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String password) {
    // Register the user in the interceptor
    authInterceptor.setUser(user, password);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configuration factory
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthenticationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // private methods -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // Specific IDao implementation -----------------------------------------------
 
  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    // web client execution
    return getResponse(new IRequest<Response<List<Arduino>>>() {
      @Override
      public Response<List<Arduino>> getResponse() {
        return webClient.getArduinos();
      }
    });
  }
}
  • líneas 19-87: estas líneas son parte de la clase [Dao] en el proyecto [client-android-skel];
  • líneas 91-100: implementación del método [getArduinos];
  • línea 94: se llama al método [getResponse] de la clase padre. El único parámetro de este método es una instancia de la interfaz [IRequest<T>];
  • líneas 95-99: el único método de la interfaz [IRequest<T>] es el método [T getResponse()];
  • línea 94: el tipo T de [IRequest<T>] debe ser el tipo T de la variable Observable<T> resultado del método en la línea 92, así que aquí, un tipo [Response<List<Arduino>>];
  • línea 97: el método [IRequest.getResponse()] delega el trabajo al método [webClient.getArduinos()] que hemos introducido. [webClient], definido en la línea 24, es instanciado por la librería AA y es una instancia de la interfaz [WebClient] que hemos introducido;

5.6.13. El [MainActivity]

  

Ya hemos presentado la actividad [MainActivity] en la sección 5.6.8. Amplía la clase [AbstractActivity] y, como tal, implementa la interfaz [IMainActivity], que a su vez amplía la interfaz [IDao]. Siempre que se añada un método a la interfaz [IDao], deberá implementarse en la clase [MainActivity]. El método [IDao.getArduinos] añadido a la interfaz [IDao] se implementará como sigue en [MainActivity]:


...
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // [DAO] layer
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
...
 
  // IDao implementation -----------------------------------------
  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    return dao.getArduinos();
  }
}
  • líneas 15-18: el método [getArduinos] se implementa delegando el trabajo a la clase [Dao] que acabamos de introducir y a la que tenemos una referencia en la línea 8;

5.6.14. El fragmento [ConfigFragment] revisitado

En la clase [ConfigFragment], el código que se ejecuta cuando se hace clic en el botón [Actualizar] es actualmente el siguiente:


  @Click(R.id.btn_Rafraichir)
  protected void doRefresh() {
    ...
    // request the list of Arduinos in the background
    getArduinosInBackground();
  }
 
  private void getArduinosInBackground() {
    // Create a dummy list of Arduinos
    List<Arduino> arduinos = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
      arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
    }
    // Simulate a server response
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // we consume it
    consumeArduinosResponse(response);
  }
 
  // display response
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    ...
}

Tenemos que reescribir las líneas 10-16, que hard-coded una respuesta de tipo [Response<List<Arduino>>]. Ahora tenemos que solicitar esta lista de la capa [DAO] a través de la actividad. El código queda como sigue:


  @Click(R.id.btn_Rafraichir)
  protected void doRefresh() {
    // check the inputs
    if (!pageValid()) {
      return;
    }
    // save the input
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // prepare to wait
    beginWaiting(1);
    // execute the asynchronous task
    executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {
 
      @Override
      public void call(Response<List<Arduino>> response) {
        // process the response
        consumeArduinosResponse(response);
      }
    });
}
  • línea 8: el URL raíz del servicio web / JSON introducido por el usuario se pasa a la capa [DAO] a través de la actividad. Este será el URL raíz de la interfaz [WebClient] (véase la sección 5.6.12.2);
  • línea 10: se notifica a la clase padre que se va a lanzar una tarea asíncrona;
  • líneas 12-19: lanzamiento de la tarea asíncrona que devolverá la lista de Arduinos conectados al servidor;
  • línea 12: llamar al método [executeInBackground] de la clase padre. Este método espera dos parámetros:
    • línea 12: el proceso a observar. Este proceso es proporcionado aquí por el método [mainActivity.getArduinos()];
    • líneas 12-19: una instancia de la interfaz [Action1<T>], donde el tipo T es el tipo proporcionado por el proceso, aquí un tipo [Response<List<Arduino>>];
  • líneas 14-18: el método llamado cuando la tarea asíncrona devuelve su resultado de tipo [Response<List<Arduino>>];
  • línea 17: la respuesta recibida se pasa al método [consumeArduinosResponse] escrito anteriormente;

Tarea: Inicie el servidor como se describe en la sección 5.4. Conecte uno o más Arduinos al PC en el que se ejecuta el servidor. A continuación, ejecute el cliente Android y compruebe que puede recuperar correctamente la lista de Arduinos conectados. Observe los registros.


Image

  • Introduzca el URL indicado en [1]. Esta es una de las direcciones IP de su servidor;
  • Haz clic en el botón [2];
  • Deberías ver la lista de Arduinos conectados en [3];

Compruebe que esta lista también aparece en las demás pestañas.

5.7. Próximos pasos


Siguiendo el mismo procedimiento utilizado para la vista [Config], implemente y, a continuación, pruebe las otras cuatro vistas de la aplicación a su vez: [Parpadeo], [PinRead], [PinWrite] y [Comandos].


Las vistas que deben crearse se presentaron en la sección 5.5.

For each view, you must:

  • dibujar la vista XML (ver Sección 5.6.9);
  • construir el fragmento asociado (véase la sección 5.6.10);
  • añadir un método a la interfaz [WebClient] (véase la sección 5.6.12.2);
  • añadir un método a la interfaz [IDao] (véase la sección 5.6.12.2);
  • añadir un método a la clase [Dao] (ver sección 5.6.12.3);
  • añadir un método a la actividad [MainActivity] (véase la sección 5.6.13);
  • escribir los controladores de eventos del fragmento (véase la sección 5.6.14);
  • prueba y observa los registros;

Nota 1: El ejemplo a seguir es el proyecto [Ejemplo-16B] del curso (véase la sección 2.8.3).

Nota 2: Los URLs a consultar y el tipo de sus respuestas se presentaron en la sección 5.4.2.

Nota 3:

La clase [CommandsFragment] envía una lista que contiene un único comando para ser ejecutado por uno o más Arduinos. Este comando se encapsulará en el siguiente [ArduinoCommand] clase:


package android.arduinos.dao;
 
import java.util.Map;
 
public class ArduinoCommand {
 
  // data
  private String id;
  private String ac;
  private Map<String, Object> pa;
 
  // constructors
  public ArduinoCommand() {
 
  }
 
  public ArduinoCommand(String id, String ac, Map<String, Object> pa) {
    this.id = id;
    this.ac = ac;
    this.pa = pa;
  }
 
  // getters and setters
...
}

En la interfaz [WebClient], el método para ejecutar esta lista de comandos será el siguiente:


  // sending JSON commands
  @Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
  • línea 2: el URL se solicita con una petición HTTP POST;
  • línea 3: el valor publicado debe tener la anotación [@Body];

Nota 4: Se recomienda abordar esta tarea del siguiente modo:

  • sólo pasará a la siguiente vista una vez que la vista actual haya sido creada y probada;
  • sólo gestione el estado de las vistas después de obtener una aplicación funcional en condiciones normales. A continuación, para cada vista, realice un ciclo a través del dispositivo para diferentes estados de la vista y anote cualquier información perdida. Estos son los datos que hay que guardar y luego restaurar. A continuación, verifique la navegación: cuando abandone una pestaña y vuelva a ella más tarde, debe estar en el mismo estado que cuando la dejó;