Skip to content

5. Trabalho 2 - Controlo de Arduinos com um tablet Android

Vamos agora aprender a controlar uma placa Arduino com um tablet. O exemplo a seguir é o projeto [client-android-skel] do curso (ver parágrafo 2).

5.1. Arquitetura do projeto

Todo o projeto terá a seguinte arquitetura:

  • O bloco [1], o servidor web/JSON e os Arduinos, serão fornecidos a si;
  • terá de construir o bloco [2], o programa para o tablet Android que se comunica com o servidor web/JSON.

5.2. Hardware

Os seguintes componentes estão à sua disposição:

  • um Arduino com um shield Ethernet, um LED e um sensor de temperatura;
  • um miniHub para partilhar com outro aluno;
  • um cabo USB para alimentar o Arduino;
  • dois cabos de rede para ligar o Arduino e o PC à mesma rede privada;
  • um tablet Android;

5.2.1. O Arduino

Veja aqui como ligar os vários componentes entre si:

  • desligue o cabo de rede do seu PC;
  • Ligue o seu PC ao Arduino utilizando um cabo de rede;
  • O Arduino que possui já estará programado. O seu endereço IP será [192.168.2.2]. Para que o seu PC reconheça o Arduino, deve atribuir-lhe um endereço IP na rede [192.168.2]. Os Arduinos foram programados para comunicar com um PC com o endereço IP [192.168.2.1]. Veja como fazê-lo:

Vá a [Painel de Controlo\Rede e Internet\Centro de Rede e Partilha]:

 
  • Em [1], clique na ligação [Rede local];
  • em [2], clique no botão [Propriedades] da rede local;
  • em [3], clique nas propriedades [IPv4] do adaptador [Conexão de Rede Local];
  • em [4], atribua a este adaptador o endereço IP [192.168.2.1] e a máscara de sub-rede [255.255.255.0];
  • em [5], clique em [OK] tantas vezes quantas forem necessárias para sair do assistente.

5.2.2. O tablet

  • Utilizando o seu adaptador Wi-Fi, ligue o seu computador à rede Wi-Fi que iremos disponibilizar. Faça o mesmo com o seu tablet;
  • Verifique o endereço IP Wi-Fi do seu PC digitando [ipconfig] numa janela do Prompt de Comando. Encontrará um endereço como [192.168.x.y];

dos>ipconfig
 
Configuration IP de Windows
 
Carte réseau sans fil Wi-Fi :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::39aa:47f6:7537:f8e1%2
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.1.25
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : 192.168.1.1
  • Verifique o endereço IP Wi-Fi do seu tablet. Se não tiver a certeza de como fazê-lo, pergunte ao seu formador. Encontrará um endereço do tipo [192.168.x.z];
  • Desative a firewall do seu PC, caso esteja ativa [Painel de Controlo\Sistema e Segurança\Firewall do Windows];
  • Numa janela do Prompt de Comando, verifique se o PC e o tablet conseguem comunicar digitando o comando [ping 192.168.x.z], onde [192.168.x.z] é o endereço IP do seu tablet. O tablet deverá então responder:
dos>ping 192.168.1.26

Envoi d'une requête 'Ping'  192.168.1.26 avec 32 octets de données :
Réponse de 192.168.1.26 : octets=32 temps=102 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=134 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=168 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=208 ms TTL=64

Statistiques Ping pour 192.168.1.26:
    Paquets : envoyés = 4, reçus = 4, perdus = 0 (perte 0%),
Durée approximative des boucles en millisecondes :
    Minimum = 102ms, Maximum = 208ms, Moyenne = 153ms

A configuração de rede do seu sistema já está pronta.

5.2.3. O emulador [Genymotion]

O emulador [Genymotion] (ver Secção 6.9) é uma excelente alternativa a um tablet. É quase tão rápido e não requer uma ligação Wi-Fi. Recomendamos a utilização deste método. Pode utilizar o tablet para o teste final da sua aplicação.

5.3. Programação de Arduinos

Aqui, focamo-nos na escrita de código C para Arduinos:

Veja também

  • Instalação do IDE de desenvolvimento do Arduino (ver Secção 6.1);
  • Utilização de bibliotecas JSON (Anexos, Secção 6.6);
  • No IDE do Arduino, teste o exemplo de um servidor TCP (por exemplo, o servidor web) e o de um cliente TCP (por exemplo, o cliente Telnet);
  • os apêndices sobre o ambiente de programação do Arduino na secção 6.1.

Um Arduino é um conjunto de pinos ligados ao hardware. Estes pinos são entradas ou saídas. Os seus valores são binários ou analógicos. Para controlar o Arduino, existem duas operações básicas:

  • escrever um valor binário/analógico num pino identificado pelo seu número;
  • ler um valor binário/analógico de um pino identificado pelo seu número;

A estas duas operações básicas, vamos adicionar uma terceira:

  • fazer um LED piscar durante um determinado período de tempo e a uma determinada frequência. Esta operação pode ser realizada chamando repetidamente as duas operações básicas anteriores. No entanto, veremos nos testes que as trocas entre a camada [DAO] e um Arduino demoram cerca de um segundo. Por isso, não é possível fazer um LED piscar a cada 100 milissegundos, por exemplo. Assim, iremos implementar esta função de piscar no próprio Arduino.

O Arduino funcionará da seguinte forma:

  • A comunicação entre a camada [DAO] e um Arduino ocorre através de uma rede TCP-IP, mediante a troca de linhas de texto no formato JSON (JavaScript Object Notation);
  • no arranque, o Arduino liga-se à porta 100 de um servidor de registo localizado na camada [DAO]. Envia uma única linha de texto para o servidor:
{"id":"cuisine","desc":"duemilanove","mac":"90:A2:DA:00:1D:A7","port":102}

Esta é uma cadeia JSON que descreve o Arduino que está a ligar-se:

  • id: um identificador para o Arduino;
  • desc: uma descrição do que o Arduino pode fazer. Aqui, especificámos simplesmente o modelo do Arduino;
  • mac: o endereço MAC do Arduino;
  • port: o número da porta na qual o Arduino aguardará comandos da camada [DAO].

Todas estas informações estão no formato de cadeia de caracteres, exceto a porta, que é um número inteiro.

  • Assim que o Arduino se registar no servidor de registo, começa a escutar na porta que especificou para o servidor (102 acima). Aguarda comandos JSON no seguinte formato:
{"id":"identifiant","ac":"une_action","pa":{"param1":"valeur1","param2":"valeur2",...}}

Esta é uma cadeia JSON com os seguintes elementos:

  • id: um identificador para o comando. Pode ser qualquer coisa;
  • ac: uma ação. Existem três:
  • pw (gravação de pino) para gravar um valor num pino,
  • pr (leitura de pino) para ler o valor de um pino,
  • cl (piscar) para fazer um LED piscar;
  • pa: os parâmetros da ação. Estes dependem da ação.
  • O Arduino devolve sempre uma resposta ao seu cliente. Trata-se de uma cadeia JSON no seguinte formato:
{"id":"1","er":"0","et":{"pinx":"valx"}}

onde

  • id: o identificador do comando ao qual se está a responder;
  • er (erro): um código de erro, caso tenha ocorrido um erro; caso contrário, 0;
  • et (status): um dicionário que está sempre vazio, exceto no comando de leitura pr. O dicionário contém então o valor do pino número x que foi solicitado.

Aqui estão alguns exemplos para esclarecer as especificações anteriores:

Fazer o LED n.º 8 piscar 10 vezes com um intervalo de 100 milissegundos:

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

Os parâmetros para o comando cl são: a duração (dur) de um flash em milissegundos, o número (nb) de flashes e o número do pino do LED.

Escreva o valor binário 1 no pino 7:

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

Os parâmetros pa do comando pw são: o modo de escrita mod (b para binário ou a para analógico), o valor val a ser gravado e o número do pino. Para uma escrita binária, val é 0 ou 1. Para uma escrita analógica, val está no intervalo [0,255].

Escreva o valor analógico 120 no pino 2:

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

Ler o valor analógico do pino 0:

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

Os parâmetros pa do comando pr são: o modo de leitura (binário ou analógico) e o número do pino. Se não houver erro, o Arduino coloca o valor do pino solicitado na chave "et" da sua resposta. Aqui, pin0 indica que o valor do pino 0 foi solicitado, e 1023 é esse valor. No modo de leitura, um valor analógico estará no intervalo [0, 1024].

Apresentámos os três comandos cl, pw e pr. Poder-se-á questionar por que razão não utilizámos campos mais explícitos nas cadeias JSON — tais como action em vez de ac, pinwrite em vez de pw e parameters em vez de pa. Um Arduino tem uma memória muito limitada. No entanto, as cadeias JSON trocadas com o Arduino contribuem para o consumo de memória. Por isso, optámos por encurtá-las o máximo possível.

Agora, vamos analisar alguns casos de erro:

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

Foi enviado um comando que não está no formato JSON. O Arduino devolveu o código de erro 100.

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

Foi enviado um comando pr sem o parâmetro pin. O Arduino devolveu o código de erro 302.

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

Enviámos um comando pinread desconhecido (é pr). O Arduino devolveu o código de erro 104.

Não vamos continuar com os exemplos. A regra é simples. O Arduino não deve falhar, independentemente do comando que lhe seja enviado. Antes de executar um comando JSON, ele garante que o comando é válido. Assim que ocorre um erro, o Arduino interrompe a execução do comando e devolve a cadeia de erro JSON ao seu cliente. Mais uma vez, como o espaço de memória é limitado, devolvemos um código de erro em vez de uma mensagem completa.

O código do programa em execução no Arduino é fornecido nos exemplos deste documento:

  

Para transferi-lo para o Arduino:

  • Ligue-o ao seu PC;
  • em [1], abra o ficheiro [arduino_uno.ino]. O Arduino IDE será iniciado e carregará o ficheiro;

Nota: O código foi originalmente criado e testado com o IDE do Arduino 1.5.x. Desde então, foram lançadas outras versões do IDE. O código não funcionou com o IDE do Arduino 1.6.x. Parece existir um problema de compatibilidade com versões anteriores entre as versões 1.6 e 1.5.

  • Em [2-4], especifique o tipo de Arduino utilizado;
  • Em [5-7], especifique a que porta série do PC está ligado;
  • em [8], carregue o programa [arduino_uno] no Arduino;

O código do programa está repleto de comentários. Os leitores interessados podem consultá-lo. Destacamos apenas as linhas de código que configuram a comunicação bidirecional cliente/servidor entre o Arduino e o PC:


#include <SPI.h>
#include <Ethernet.h>
#include <ajSON.h>
 
// ---------------------------------- CONFIGURATION DE L'ARDUINO UNO
// adresse MAC de l'Arduino UNO
byte macArduino[] = { 
  0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// l'adresse IP de l'Arduino
IPAddress ipArduino(192,168,2,2);
// son identifiant
char * idArduino="cuisine";
// port du serveur Arduino
int portArduino=102;
// description de l'Arduino
char * descriptionArduino="contrôle domotique";
// le serveur Arduino travaillera sur le port 102
EthernetServer server(portArduino);
// IP du serveur d'enregistrement
IPAddress ipServeurEnregistrement(192,168,2,1); 
// port du serveur d'enregistrement
int portServeurEnregistrement=100;
// le client Arduino du serveur d'enregistrement
EthernetClient clientArduino;
// la commande du client
char commande[100];
// la réponse de l'Arduino
char message[100];
 
// initialisation
void setup() {
  // Le moniteur série permettra de suivre les échanges
  Serial.begin(9600);
  // démarrage de la connection Ethernet
  Ethernet.begin(macArduino,ipArduino);  
  // mémoire disponible
  Serial.print(F("Memoire disponible : "));
  Serial.println(freeRam());
}
 
// boucle infinie
void loop()
{
  ...
}
  • Linha 8: o endereço MAC do Arduino. Isso não importa muito aqui, porque o Arduino estará numa rede privada com um PC e um ou mais Arduinos. O endereço MAC precisa apenas de ser único nesta rede privada. Normalmente, a placa de rede do Arduino tem um autocolante que indica o endereço MAC da placa. Se este autocolante estiver em falta e não souber o endereço MAC da placa, pode introduzir o que quiser na linha 8, desde que seja respeitada a regra da exclusividade do endereço MAC na rede privada;
  • linha 11: o endereço IP da placa. Mais uma vez, pode introduzir qualquer valor do tipo [192.168.2.x] e variar o x para os diferentes Arduinos na rede privada;
  • Linha 13: identificador do Arduino. Deve ser único entre os identificadores dos Arduinos na mesma rede privada;
  • linha 15: a porta de serviço do Arduino. Pode introduzir o que quiser;
  • linha 17: descrição da função do Arduino. Pode introduzir o que quiser. Tenha cuidado com cadeias de caracteres longas devido à memória limitada do Arduino;
  • linha 21: endereço IP do servidor de registo do Arduino no PC. Não deve ser alterado;
  • linha 23: porta para este serviço de registo. Não deve ser alterada;

5.4. O servidor Web/JSON

5.4.1. Instalação

Image

O binário Java para o servidor Web/JSON é fornecido:

 

Abra um prompt de comando e digite o seguinte comando:

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

Se o [java.exe] não estiver no PATH do prompt de comando, terá de digitar o caminho completo para o [java.exe] (normalmente C:\Program Files\java\...).

Uma janela do DOS irá abrir e exibir os registos:


.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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] o.apache.catalina.core.StandardService   : Starting service Tomcat
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.faireClignoterLed(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}/{valeur}],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
Serveur d'enregistrement lancÚ sur 192.168.2.1:100
2014-01-06 11:11:38.101  INFO 8408 --- [       Thread-4] arduino.dao.Recorder                  : Recorder : [11:11:38:101] : [Serveur d'enregistrement : attente d'un client]
2014-01-06 11:11:38.142  INFO 8408 --- [           main] arduino.rest.metier.Application : Started Application in 3.257 seconds
  • linha 11: um servidor Tomcat incorporado é iniciado;
  • linha 15: o servlet Spring MVC [dispatcherServlet] é carregado e executado;
  • linha 18: a URL REST [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}] é detetada;
  • linha 19: a URL REST [/arduinos/commands/{idArduino}] é detetada;
  • linha 20: a URL REST [/arduinos/] é detetada;
  • linha 21: a URL REST [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}] é detetada;
  • linha 22: a URL REST [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}] é detetada;
  • linha 26: o servidor de registo do Arduino é iniciado;

Ligue o seu Arduino ao PC, caso ainda não o tenha feito. A firewall do PC deve estar desativada. Em seguida, num navegador da Web, introduza a URL [http://localhost:8080/arduinos]:

Deve ver aparecer o ID do Arduino ligado. Se nada aparecer, tente reiniciar o Arduino. Este possui um botão de reinicialização para esse efeito.

O servidor web/JSON está agora instalado.

5.4.2. As URLs disponibilizadas pelo serviço web/JSON

Consulte: projeto [Exemplo-15] (ver secção 1.16.1);

O serviço web/JSON foi implementado utilizando o Spring MVC e expõe as seguintes URLs:


@Controller
public class WebController {
 
  // business layer
  @Autowired
  private IMetier métier;
 
  // list of arduinos
  @RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String getArduinos() throws JsonProcessingException {
    ...
  }
 
  // flashing
  @RequestMapping(value = "/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String faireClignoterLed(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("duree") int duree, @PathVariable("nombre") int nombre) throws JsonProcessingException {
...
  }
 
  // order dispatch JSON
  @RequestMapping(value = "/arduinos/commands/{idArduino}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String sendCommandesJson(@PathVariable("idArduino") String idArduino, HttpServletRequest request) throws IOException {
    ...
  }
 
  // pin reading
  @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 {
    ....
  }
 
  // writing pin
  @RequestMapping(value = "/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}", 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("valeur") int valeur) throws JsonProcessingException {
  ...
  }
}

As respostas enviadas pelo servidor são representações JSON da seguinte classe [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;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

A URL [/arduinos] devolve uma resposta do tipo [Response<List<Arduino>>], em que [Arduino] é a seguinte classe:


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
...
}
  • linha 7: [id] é o identificador do Arduino;
  • linha 8: a sua descrição;
  • linha 9: o seu endereço MAC;
  • linha 10: o seu endereço IP;
  • linha 11: a porta na qual ele escuta comandos;

Os URLs:

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

retornar uma resposta do tipo [Response<ArduinoResponse>], em que a classe [ArduinoResponse] representa a resposta padrão do Arduino:


public class ArduinoResponse implements Serializable {
 
  private String json;
  private String id;
  private String erreur;
  private Map<String, Object> etat;
 
  // getters and setters
...
}
  • [json]: a cadeia JSON enviada por um Arduino que não foi possível descodificar (em caso de erro); caso contrário, é nulo;
  • [id]: o identificador do comando ao qual o Arduino está a responder;
  • [error]: um código de erro, 0 se tudo estiver OK, caso contrário, outro valor;
  • [status]: um dicionário contendo a resposta específica para o comando. Normalmente está vazio, a menos que o comando tenha solicitado a leitura de um valor do Arduino; nesse caso, esse valor será colocado neste dicionário;

5.4.3. Teste do serviço web / JSON

Familiarize-se com o servidor web / JSON testando os seguintes URLs:

URL
função
http://localhost:8080/arduinos/
Retorna a lista de Arduinos conectados
http://localhost:8080/arduinos/
blink/1/kitchen/8/100/20/
faz com que o LED no pino 8
 no Arduino identificado como «cuisine»,
 20 vezes a cada 100 ms.
http://localhost:8080/arduinos/
pinRead/1/cuisine/0/a/
leitura analógica do pino 0 do
 Arduino identificado como "kitchen"
http://localhost:8080/arduinos/
pinRead/1/kitchen/5/b/
leitura binária do pino 5 do
 Arduino identificado por cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/8/b/1/
Escreve o valor binário 1 no pino 8 do Arduino identificado por
 cuisine
http://localhost:8080/arduinos/
pinWrite/1/kitchen/4/a/100/
Gravação analógica do valor 100 no pino 4 do Arduino identificado
 pela cozinha

Aqui estão algumas capturas de ecrã do que deverá ver:

Obter a lista de Arduinos ligados:

A cadeia JSON recebida do servidor web / JSON é um objeto com os seguintes campos:

  • [status]: 0 indica que não houve erro — caso contrário, ocorreu um erro;
  • [messages]: uma lista de mensagens explicando o erro, caso tenha ocorrido um erro:
  • [body]: a lista de Arduinos, caso não tenha ocorrido nenhum erro. Cada Arduino é então descrito por um objeto com os seguintes campos:
    • [id]: o identificador do Arduino. Não podem existir dois Arduinos com o mesmo identificador;
    • [description]: uma breve descrição da funcionalidade do Arduino;
    • [mac]: o endereço MAC do Arduino;
    • [ip]: o endereço IP do Arduino;
    • [port]: a porta na qual ele escuta comandos;

Faça com que o LED no pino 8 do Arduino identificado por [cuisine] pisque 20 vezes a cada 100 ms:

 

A cadeia JSON recebida do servidor web / JSON é um objeto com os seguintes campos:

  • [status]: 0 indica que não houve erro — caso contrário, ocorreu um erro;
  • [messages]: uma lista de mensagens explicando o erro, caso tenha ocorrido um erro:
  • [body]: a resposta do Arduino se não houve erro:
    • [id]: identificador do comando. Este identificador é o 1 em [/blink/1]. O Arduino inclui este identificador de comando na sua resposta;
    • [error]: um número de erro. Um valor diferente de 0 indica um erro;
    • [state]: utilizado apenas para ler um pino. O seu valor é o valor do pino;
    • [json]: utilizado apenas no caso de um erro JSON entre o cliente e o servidor. O seu valor é a cadeia JSON errada enviada pelo Arduino;

Leitura analógica do pino 0 no Arduino, identificada por [kitchen]:

 

A cadeia JSON recebida do servidor web /json é semelhante à anterior, com exceção do campo [state], que representa o valor do pino 0.

Leitura binária do pino 5 no Arduino, identificada por [kitchen]:

 

A cadeia JSON recebida do servidor web /json é semelhante à anterior.

Gravação binária do valor 1 no pino 8 do Arduino identificado por [kitchen]:

 

A cadeia JSON recebida do servidor web /json é semelhante à anterior.

Testar a URL [http://localhost:8080/arduinos/commands/cuisine] é mais complicado. O método /json do servidor web que lida com esta URL espera um pedido POST, o que não pode ser facilmente simulado utilizando um navegador. Para testar esta URL, pode utilizar um navegador Chrome com a extensão [Advanced REST Client] (ver secção 6.13):

 
  • em [1], o URL do método web/JSON a ser testado;
  • em [2], o método POST para enviar o pedido;
  • em [3-4], o valor enviado é JSON;
  • em [5], a cadeia JSON que está a ser enviada. Repare nos parênteses retos que iniciam e terminam a lista. Aqui, a lista contém apenas um comando JSON que faz com que o pino 8 pisque 10 vezes a cada 100 ms;
  • em [6], a solicitação é enviada;
 
  • em [7], a resposta JSON enviada pelo servidor. O objeto recebeu um objeto com os dois campos habituais [status, messages] e um campo [body] cujo valor é a lista de respostas do Arduino a cada um dos comandos JSON enviados.

Vamos ver o que acontece quando enviamos um comando JSON com sintaxe incorreta para o Arduino:

Recebemos então a seguinte resposta:

 

Podemos ver que, na resposta do Arduino, o código de erro é [104], indicando que o comando [xx] não foi reconhecido.

5.5. Teste do cliente Android

O executável final do cliente Android é fornecido abaixo:

  

Utilize o rato para arrastar o ficheiro [app-debug.apk] acima para um emulador de tablet [GenyMotion]. Este será então guardado e executado. Inicie também o servidor web/jSON, caso ainda não o tenha feito. Ligue o Arduino ao PC com um LED ligado ao mesmo. O cliente Android permite-lhe gerir Arduinos remotamente. Apresenta os seguintes ecrãs ao utilizador.

O separador [CONFIG] permite-lhe ligar-se ao servidor e recuperar a lista de Arduinos ligados:

Image

  • Em [1], introduza o endereço IP [192.168.2.1] atribuído ao seu PC (consulte a secção 5.2).

O separador [PINWRITE] permite-lhe escrever um valor num pino do Arduino:

Image

Image

O separador [PINREAD] permite-lhe ler o valor de um pino do Arduino:

Image

O separador [BLINK] permite-lhe fazer um LED do Arduino piscar:

Image

O separador [COMMAND] permite enviar um comando JSON para um Arduino:

Image

5.6. O cliente Android para o serviço web / JSON

Vamos agora discutir a criação do cliente Android.

5.6.1. Arquitetura do cliente

A arquitetura do cliente Android será a do projeto [Exemplo-15] (ver secção 1.16.2);

  • a camada [DAO] comunica com o servidor web/JSON;

O cliente Android deve ser capaz de controlar vários Arduinos simultaneamente. Por exemplo, queremos poder fazer com que dois LEDs em dois Arduinos pisquem ao mesmo tempo, e não um após o outro. Por isso, o nosso cliente Android utilizará uma tarefa assíncrona por Arduino, e estas tarefas serão executadas em paralelo.

5.6.2. O projeto do cliente Android Studio

Duplique o projeto [client-android-skel] (consulte a secção 2) para o projeto [client-arduinos-01] (se necessário, reveja como duplicar um projeto Gradle na secção 1.15):

5.6.3. As cinco vistas XML

  

Haverá cinco vistas XML:

  • [blink]: para fazer um LED do Arduino piscar. Está associada ao fragmento [BlinkFragment];
  • [commands]: para enviar um comando JSON a um Arduino. Está associada ao fragmento [CommandsFragment];
  • [config]: para configurar o serviço web/URL JSON e recuperar a lista inicial de Arduinos conectados. Está associada ao fragmento [ConfigFragment];
  • [pinread]: para ler o valor binário ou analógico de um pino do Arduino. Está associado ao fragmento [PinReadFragment];
  • [pinwrite]: para escrever um valor binário ou analógico num pino do Arduino. Está associado ao fragmento [PinWriteFragment];

Por enquanto, estas cinco vistas XML terão todas o mesmo conteúdo vazio:


<?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>
  • A vista encontra-se dentro de um contentor [RelativeLayout] (linhas 7–10), que por sua vez está contido num contentor [ScrollView] (linhas 2–11). Isto garante que podemos percorrer a vista se esta exceder o tamanho do ecrã de um tablet;

Tarefa: Crie as cinco vistas XML.


5.6.4. O menu de fragmentos

Sabemos que os fragmentos num projeto construído com [client-android-skel] devem estar associados a um menu, mesmo que este esteja vazio. Aqui, a aplicação não terá um menu. O menu vazio já se encontra no projeto;

  

5.6.5. Os cinco componentes da aplicação

 

Tarefa: Duplique o fragmento [DummyFragment] para os cinco fragmentos da aplicação, conforme mostrado em [2].


O fragmento [ConfigFragment] tem a seguinte estrutura:


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 parent class -------------------------------------------------------
...

Substitua a linha 10 pela seguinte linha:


@EFragment(R.layout.config)

Tarefa: Faça o mesmo para os outros quatro fragmentos, adaptando o atributo [@EFragment] da classe.


Fragmento
View
ConfigFragment

R.layout.config
Fragmento de leitura de PIN

R.layout.pinread
PinWriteFragment

R.layout.pinwrite
Fragmento de comandos

R.layout.commands
Fragmento de piscar

R.layout.blink

5.6.6. Estados do fragmento

Cada fragmento terá um estado.


Tarefa: Duplique a classe [DummyFragmentState] cinco vezes para criar os cinco estados apresentados em [2].


5.6.7. Personalização do projeto

 

O pacote [architecture / custom] contém os elementos personalizáveis da arquitetura da aplicação.

5.6.7.1. A interface [IMainActivity]

A interface [IMainActivity] define o que os fragmentos podem solicitar à atividade, bem como as constantes da aplicação. Esta interface será a seguinte:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer 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;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of fragments
  int FRAGMENTS_COUNT = 5;
 
  // view n°s
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}
  • linhas 25, 28, 31, 40: configuração da camada [DAO]. Esta aplicação consulta um servidor web/JSON;
  • linha 37: esta aplicação tem separadores;
  • linha 43: esta aplicação tem cinco fragmentos;
  • linhas 46–50: os números dos cinco fragmentos;
  • linha 34: adjacência de fragmentos. O programador pode definir aqui um valor dentro do intervalo [1, FRAGMENTS_COUNT-1];

5.6.7.2. A classe [CoreState]

A classe [CoreState] é a classe pai dos estados dos fragmentos:


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;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • linhas 12–16: as classes de estado para os cinco fragmentos devem ser declaradas aqui;

5.6.8. A classe [MainActivity]

  

A classe [MainActivity] terá a seguinte forma:


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 {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // creation of 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_titre).toUpperCase(l);
      case 1:
        return getString(R.string.blink_titre).toUpperCase(l);
      case 2:
        return getString(R.string.pinread_titre).toUpperCase(l);
      case 3:
        return getString(R.string.pinwrite_titre).toUpperCase(l);
      case 4:
        return getString(R.string.commands_titre).toUpperCase(l);
    }
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // fragment n° position is displayed
    navigateToView(position, ISession.Action.NAVIGATION);
  }
 
  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }
 
  // implémentation IDao -----------------------------------------
}
  • linhas 46–50: criação dos cinco separadores da aplicação;
  • linha 48: os títulos das guias são fornecidos pelo método nas linhas 63-79;
  • os cinco fragmentos são instanciados na linha 60. Devido às anotações AA, as classes dos fragmentos são as apresentadas anteriormente, com um sublinhado como sufixo;
  • linhas 63-79: é definido um título para cada fragmento. Estes títulos serão recuperados do ficheiro [res/values/strings.xml]
  

O conteúdo do ficheiro [strings.xml] é o seguinte:


<?xml version="1.0" encoding="utf-8"?>
<resources>
 
  <!-- application name -->
  <string name="app_name">[arduinos-client-01]</string>
  <!-- Fragments and tabs -->
  <string name="config_titre">[Config]</string>
  <string name="blink_titre">[Blink]</string>
  <string name="pinread_titre">[PinRead]</string>
  <string name="pinwrite_titre">[PinWrite]</string>
  <string name="commands_titre">[Commands]</string>
 
</resources>

Tarefa: Crie os elementos listados acima e compile o projeto. Não deve haver erros.


Execute o projeto. Deverá ver a seguinte visualização no emulador:

Image

Examine os registos que acompanharam a exibição da primeira vista e acompanhe os vários passos que foram executados. Alterne entre os separadores e continue a acompanhar os registos.

5.6.9. A vista XML [config]

A vista XML [config] terá o seguinte aspeto:

A visualização acima é gerada pelo seguinte 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/titre"/>
 
    <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/titre_list_arduinos"
      android:textColor="@color/blue"
      android:textSize="20sp"/>
 
    <Button
      android:id="@+id/btn_Rafraichir"
      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_rafraichir"/>
 
    <Button
      android:id="@+id/btn_Annuler"
      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_annuler"
      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>

A vista utiliza strings (android:text nas linhas 15, 25, 37, 50, 61, 73) que estão definidas no ficheiro [res/values/strings]:

  

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <string name="app_name">android-domotique</string>
 
    <!-- Fragments and tabs -->
    <string name="config_titre">[Config]</string>
    <string name="blink_titre">[Blink]</string>
    <string name="pinread_titre">[PinRead]</string>
    <string name="pinwrite_titre">[PinWrite]</string>
    <string name="commands_titre">[Commands]</string>
 
    <!-- Config -->
    <string name="txt_TitreConfig">Se connecter au serveur</string>
    <string name="txt_UrlServiceRest">Url du service web / jSON</string>
    <string name="txt_MsgErreurUrlServiceRest">L\'Url du service doit être entrée sous la forme Ip1.Ip2.Ip3.IP4:Port/contexte</string>
    <string name="hint_UrlServiceRest">ex (192.168.1.120:8080/rest)</string>
    <string name="btn_annuler">Annuler</string>
    <string name="btn_rafraichir">Rafraîchir</string>
    <string name="titre_list_arduinos">Liste des Arduinos connectés</string>
 
</resources>

A vista utiliza cores (android:textColor nas linhas 51 e 62) definidas no ficheiro [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>

A vista utiliza dimensões (android:textSize na linha 16) que estão definidas no ficheiro [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>
  <!-- appli -->
  <dimen name="titre">30dp</dimen>
</resources>

Esta técnica não foi utilizada para todas as dimensões. No entanto, é a abordagem recomendada. Permite-lhe alterar as dimensões num único local.


Tarefa: Crie os elementos acima.


Execute o seu projeto novamente. Deverá ver a seguinte vista:

Image

5.6.10. O fragmento [ConfigFragment]

Para lidar com a nova vista [config], o código do fragmento [ConfigFragment] é alterado da seguinte forma:


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_vide)
public class ConfigFragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.btn_Rafraichir)
  protected Button btnRafraichir;
  @ViewById(R.id.btn_Annuler)
  protected Button btnAnnuler;
  @ViewById(R.id.edt_UrlServiceRest)
  protected EditText edtUrlServiceRest;
  @ViewById(R.id.txt_MsgErreurIpPort)
  protected TextView txtMsgErreurUrlServiceRest;
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
 
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  }
 
  // 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) {
    // 1st 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) {
  }
 
  // méthodes privées --------------------------------------------
 
  private void initButtons() {
    // the [Execute] button replaces the [Cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
}
  • linhas 23–32: os elementos da interface visual;
  • linhas 58–60: na primeira visita ao fragmento, a mensagem de erro é ocultada;
  • linhas 73–76: cada vez que o fragmento é exibido, o botão [Cancelar] é ocultado (linha 82) e o botão [Atualizar] é exibido (linhas 86–87). De facto, nesta aplicação, um fragmento não pode ser exibido enquanto uma operação assíncrona está em curso e, por isso, o botão [Cancelar] fica visível;

Tarefa: Crie os elementos acima referidos.


Execute esta nova versão. A primeira vista deverá agora ter este aspeto:

Image

5.6.10.1. O botão [Atualizar]

Por enquanto, vamos tratar o clique no botão [Atualizar] da seguinte forma:


@Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // we're going to launch a task - we're preparing the wait
    beginWaiting(1);
  }
 
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
  }
 
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to wait for tasks
    beginRunningTasks(numberOfRunningTasks);
    // the [Cancel] button replaces the [Refresh] button
    btnRafraichir.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
}
  // fragment lifecycle management -------------------------------------
...
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // buttons in their original state
    initButtons();
  }
 
  // méthodes privées --------------------------------------------
 
  private void initButtons() {
    // the [Execute] button replaces the [Cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
  • linhas 1-5: o método executado quando o botão [Atualizar] é clicado;
  • linha 4: iniciamos a espera;
  • linha 18: passamos o número de tarefas assíncronas a serem iniciadas para a classe pai. A imagem de carregamento aparecerá;
  • linhas 20-21: esta espera fará com que o botão [Cancel] apareça, o botão [Refresh] desapareça e a imagem de carregamento apareça. Nada mais acontece. No entanto, o utilizador pode clicar no botão [Cancel]. O método nas linhas 7-14 será então executado;
  • linha 13: é solicitado à classe pai que cancele todas as tarefas. A classe fará isso e, por sua vez, chamará o método nas linhas 25–29 para sinalizar que todas as tarefas foram concluídas. O parâmetro [runningTasksHaveBeenCanceled] terá o valor true para indicar que as tarefas foram canceladas;
  • Linhas 35–36: O botão [Cancelar] desaparecerá, enquanto o botão [Atualizar] reaparecerá.

Tarefa: Efetue estas alterações e, em seguida, execute o projeto. Verifique se o botão [Refresh] inicia a espera e se o botão [Cancel] a interrompe. Observe os registos.


5.6.10.2. Validação de entrada

Na versão anterior, não validávamos o URL introduzido. Para o validar, adicionamos o seguinte código em [ConfigFragment]:


// entered values
  private String urlServiceRest;
 
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // check entries
    if (!pageValid()) {
      return;
    }
    // we're going to launch a task - we're preparing the wait
    beginWaiting(1);
  }
 
  // input verification
  private boolean pageValid() {
    // initially no error msg
    txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    // retrieve server IP and port
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    // we 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) {
      // error msg display
      txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
      // back to UI
      return false;
    }
    // it's good
    return true;
  }
  • linha 2: o URL introduzido;
  • linhas 7–9: antes de fazer qualquer coisa, verificamos a validade da entrada;
  • linha 19: recuperamos a URL introduzida e adicionamos-lhe o prefixo [http://];
  • linha 22: tentamos construir um objeto URI (Uniform Resource Identifier) com ela. Se a URL introduzida estiver sintaticamente incorreta, será lançada uma exceção;
  • linhas 23–27: é lançada uma exceção se o URI for válido, mas [host==null] e [port==-1]. Este é um cenário possível;
  • linha 30: ocorreu uma exceção. A mensagem de erro é exibida;
  • linha 32: devolvemos [false] para indicar que a página é inválida;
  • linha 35: não ocorreram erros. Devolvemos [true] para indicar que a página é válida;

Tarefa: Implemente a funcionalidade acima.


Teste esta nova versão e verifique se os URLs inválidos são devidamente sinalizados.

5.6.10.3. Exibição da lista de Arduinos

  

As diferentes vistas terão de apresentar a lista de Arduinos ligados. Para tal, iremos definir diferentes classes e uma vista XML:

  • Um Arduino será representado pela classe [Arduino] [1];
  • a classe [CheckedArduino] [1] herda da classe [Arduino], à qual adicionámos um valor booleano para indicar se o Arduino foi selecionado numa lista;

A classe [Arduino] é a que já é utilizada pelo servidor e apresentada na secção 5.4.2. É a seguinte:


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
...
}
  • linha 7: [id] é o identificador do Arduino;
  • linha 8: a sua descrição;
  • linha 9: o seu endereço MAC;
  • linha 10: o seu endereço IP;
  • linha 11: a porta na qual ele escuta comandos;

Esta classe corresponde à cadeia JSON recebida do servidor ao solicitar a lista de Arduinos conectados:

A classe [CheckedArduino] herda da classe [Arduino]:


package android.arduinos.entities;
 
public class CheckedArduino extends Arduino {
    private static final long serialVersionUID = 1L;
    // an Arduino can be selected
    private boolean isChecked;
 
    // manufacturer
    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;
    }
 
}
  • Linha 3: A classe [CheckedArduino] herda da classe [Arduino];
  • linha 6: adicionamos uma variável booleana que nos dirá se um Arduino foi selecionado da lista de Arduinos apresentada;

Em [ConfigFragment], simularemos a recuperação da lista de Arduinos conectados.

  

  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
..
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // check entries
    if (!pageValid()) {
      return;
    }
    // we're going to launch a task - we're preparing the wait
    beginWaiting(1);
    // we clean up the Arduinos list
    clearArduinos();
    // request the list of Arduinos running in the background
    getArduinosInBackground();
  }
 
  private void getArduinosInBackground() {
   ...
  }
 
  // raz list of Arduinos
  private void clearArduinos() {
    // create an empty list
    List<String> strings = new ArrayList<>();
    // we display it
    listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
  • linha 2: o ListView que exibe os Arduinos ligados ao servidor;
  • linha 5: o método que recupera a lista de Arduinos conectados;
  • linha 11: informamos à classe pai que vamos iniciar uma tarefa assíncrona;
  • linha 12: limpamos a lista de Arduinos atualmente exibida;
  • linha 15: solicitamos a lista de Arduinos conectados como uma tarefa em segundo plano;
  • linhas 23–28: o método que limpa a lista de Arduinos atualmente exibida;

O método [getArduinosInBackground] é o seguinte:


  private void getArduinosInBackground() {
    // create a fictitious arduino list
    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));
    }
    // we simulate a server response
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // we cancel the wait
    cancelWaitingTasks();
    // change the buttons
    initButtons();
    // we consume the answer
    consumeArduinosResponse(response);
}
  • linhas 3–6: criamos uma lista de 20 Arduinos;
  • linhas 8-9: construímos a resposta do tipo [Response<List<Arduino>>] (secção 5.4.2) que irá encapsular a lista de Arduinos criada;
  • linha 11: cancelar a espera;
  • linha 13: reinicializar os botões para o seu estado inicial;
  • linha 15: consumir a resposta;

O método [consumeArduinosResponse] é o seguinte:


  // response display
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // mistake?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // back to Ui
      return;
    }
    // we create a list of [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // we display them
    showArduinos(checkedArduinos);
}
  • linhas 4-11: verificamos o código de erro na resposta enviada pelo servidor:
  • linha 4: se o código de erro não for zero;
  • linha 6: exibe as mensagens armazenadas pelo servidor no campo [messages] da resposta;
  • linha 8: regressar à interface do utilizador;
  • linhas 11-16: se não houver erros, exiba a lista de Arduinos recebidos, após convertê-la para o tipo List<CheckedArduino>;

O método [showArduinos] é o seguinte:


  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());
    }
    // we display it
    listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}

Tarefa: Efetue as alterações acima e execute o seu projeto.


Deve ver a seguinte vista quando clicar no botão [Atualizar]:

Image

A entrada em [1] não é utilizada. Por isso, pode introduzir qualquer coisa, desde que siga o formato esperado.

5.6.10.4. Um modelo para apresentar um Arduino

Atualmente, os Arduinos ligados são apresentados na vista [Config] da seguinte forma:

Image

Agora queremos exibi-los da seguinte forma:

Image

  • em [1], uma caixa de seleção que permitirá selecionar um Arduino. Esta caixa de seleção ficará oculta quando pretender exibir uma lista de Arduinos não selecionáveis;
  • em [2], o ID do Arduino;
  • em [3], a sua descrição;

O que se segue baseia-se nos conceitos desenvolvidos nos projetos [exemplo-19] e [exemplo-19B] na Secção 1.20. Reveja-os, se necessário.

Primeiro, criamos a vista que irá exibir um item da lista de Arduinos:

 

O código para a vista [listarduinos_item] acima é o seguinte:


<?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>
  • linhas 9–15: a caixa de seleção;
  • linhas 17-23: o texto [Id: ];
  • linhas 25-33: o ID do Arduino será inserido aqui;
  • linhas 35-43: o texto [Descrição: ];
  • linhas 45-53: a descrição do Arduino será inserida aqui;

Esta vista utiliza texto (linhas 23, 32, 43) definido em [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>

A vista também utiliza uma cor (linhas 33, 53) definida em [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>

O gestor de visualização para um item na lista do Arduino

  

A classe [ListArduinosAdapter] é a classe chamada pela [ListView] para apresentar cada item da lista do Arduino. O seu código é o seguinte:


package istia.st.android.vues;
 
import istia.st.android.R;
...
 
public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {
 
    // the arduino board
    private List<CheckedArduino> arduinos;
    // execution context
    private Context context;
    // the layout id for displaying a line in the arduino list
    private int layoutResourceId;
    // whether or not the line contains a checkbox
    private Boolean selectable;
 
    // manufacturer
    public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
        // parent
        super(context, layoutResourceId, arduinos);
        // memorize information
        this.arduinos = arduinos;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.selectable = selectable;
    }
 
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • linha 18: o construtor da classe recebe quatro parâmetros: a atividade atualmente em execução, o ID da vista a ser exibida para cada item na fonte de dados, a fonte de dados que preenche a lista e um valor booleano indicando se a caixa de seleção associada a cada Arduino deve ser exibida ou não;
  • Linhas 8–15: Estas quatro informações são armazenadas localmente;

Linha 29: O método [getView] é responsável por gerar a vista #[position] na [ListView] e por tratar os seus eventos. O seu código é o seguinte:


@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 on [TextView]
        TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
        TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
        // fill in the line
        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) {
            // we assign its value
            ck.setChecked(arduino.isChecked());
            // we manage the click
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
        }
        // we return the line
        return row;
    }
  • linha 2: o primeiro parâmetro é a posição na [ListView] da linha a ser criada. É também a posição na lista de Arduinos armazenada localmente;
  • linha 4: recuperamos uma referência ao Arduino que será associado à linha construída;
  • linha 6: a linha atual é construída a partir da vista [listarduinos_item.xml];
  • linhas 8–9: são recuperadas as referências aos dois [TextView]s;
  • linhas 11-12: são atribuídos valores aos dois [TextView]s;
  • linha 14: é recuperada uma referência à caixa de seleção;
  • linha 15: torna-se visível ou não, dependendo do valor [selectable] inicialmente passado ao construtor;
  • linha 16: se a caixa de seleção estiver presente;
  • linha 18: o valor [isChecked] do Arduino atual é atribuído a ela;
  • linhas 20–26: tratamos do clique na caixa de seleção;
  • linha 23: o valor da caixa de seleção é armazenado no Arduino atual;

Gerir a lista de Arduinos

A exibição da lista de Arduinos é atualmente tratada por dois métodos da classe [ConfigFragment]:

  • [clearArduinos]: que exibe uma lista vazia;
  • [showArduinos]: que exibe a lista devolvida pelo servidor;

Estes dois métodos funcionam da seguinte forma:


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

Tarefa: Efetue estas alterações e teste a nova aplicação.


Image

5.6.10.5. A sessão

A sessão é onde armazenamos as informações partilhadas entre os fragmentos e a atividade. Todos os fragmentos precisam de apresentar a lista de Arduinos ligados. Assim, uma versão inicial da sessão teria o seguinte aspeto:


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 activities
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
  // don't forget the getters and setters required for serialization / deserialization jSON
 
  // the Arduinos list
  private List<CheckedArduino> checkedArduinos = new ArrayList<>();
 
  // getters and setters
...
}

Tarefa: Crie a classe [Session] apresentada acima.


A criação desta sessão requer que modifiquemos o código existente da seguinte forma:


  // response display
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // mistake?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // cancellation
      doAnnuler();
      // back to Ui
      return;
    }
    // we create a list of [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // we put it in session
    session.setCheckedArduinos(checkedArduinos);
    // we display them
    showArduinos(checkedArduinos);
    // we cancel the wait
    cancelWaitingTasks();
}
  • linha 18: a lista de Arduinos criada pelas linhas anteriores é colocada na sessão;

5.6.10.6. Gestão do estado do fragmento

Quando o dispositivo é rodado, os componentes visuais da vista são renderizados (por predefinição) no estado em que se encontravam quando a vista foi desenhada:

  • a [ListView] contém os itens que o designer colocou lá;
  • a mensagem de erro encontra-se no estado visível ou não visível em que o designer a colocou;

Os estados dos componentes visuais no momento do design podem ou não ser adequados ao restaurar um fragmento. Qual é o caso aqui?

  • o [ListView] deve exibir a lista de Arduinos conectados. O valor do [ListView] no momento do design não pode, portanto, ser utilizado;
  • O [TextView] para a mensagem de erro deve ser restaurado para o estado visível ou oculto que tinha no momento do salvamento. O seu valor no momento do design pode não ser adequado para estes dois casos;

Devemos, portanto, guardar o estado destes dois componentes ao guardar o estado do fragmento:

  • a lista de Arduinos conectados;
  • a visibilidade (mostrada/oculta) da mensagem de erro ao introduzir o URL do serviço web/JSON;

Uma vez que a lista de Arduinos está presente na sessão, será guardada automaticamente. A visibilidade da mensagem de erro será armazenada na seguinte classe [ConfigFragmentState]:

  

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

Tarefa: Crie a classe [ConfigFragmentState] apresentada acima.


Para restaurar corretamente os estados dos fragmentos, os seus métodos [getNumView] e [saveFragment] devem ser modificados. Por exemplo, o método para o fragmento [BlinkFragment] está atualmente definido da seguinte forma:


  @Override
  public CoreState saveFragment() {
    // save the fragment
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
  }
 
  @Override
  protected int getNumView() {
    // return the fragment number in the table of fragments managed by the activity (cf MainActivity)
    return 0;
}

Se nada for feito, o estado renderizado na linha 6 será guardado no elemento 0 (linha 13) da matriz CoreState[] coreStates da classe [AbstractSession] (linha 5 abaixo):


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

No entanto, deve ser guardado no elemento correspondente ao ID do fragmento [BlinkFragment] na matriz de fragmentos definida na classe [MainActivity] (linha 9 abaixo):


@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_()};
  }
 
 

Os IDs dos fragmentos foram definidos na interface [IMainActivity]:


public interface IMainActivity extends IDao {
 
  ...
 
  // view n°s
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}

Em última análise, o estado do fragmento [BlinkFragment] será gerido corretamente se escrevermos:


  @Override
  public CoreState saveFragment() {
    // save the fragment
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
  }
 
  @Override
  protected int getNumView() {
    // return the fragment number in the table of fragments managed by the activity (cf MainActivity)
    return IMainActivity.VUE_BLINK;
}
  • Linha 14: Retorna o ID do fragmento [BlinkFragment] na matriz de fragmentos gerida pela atividade;

Além disso, a classe [CoreState], que é a classe pai dos estados dos fragmentos, apresenta-se atualmente da seguinte forma (ver secção 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;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
....
}
  • Linhas 12–16: A classe [DummyFragmentState] não está listada entre as classes filhas da classe [CoreState]. No entanto, o método [saveFragment] da classe [BlinkFragment] devolve atualmente um tipo [DummyFragmentState]. Se mantido tal como está, a serialização/desserialização da sessão falhará e a sessão não será restaurada, levando a uma falha da aplicação;

O método [saveFragment] do fragmento [BlinkFragment] deve ser reescrito da seguinte forma:


  @Override
  public CoreState saveFragment() {
    // save the fragment
    BlinkFragmentState state=new BlinkFragmentState();
    // ...
    return state;
    // if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
}

Tarefa: Em cada fragmento, modifique o método [getNumView] para que retorne o número do fragmento e o método [saveFragment] para que retorne uma instância da classe de estado do fragmento (conforme mostrado acima).


5.6.10.7. Gestão do ciclo de vida do fragmento

Aqui, focamo-nos no ciclo de vida do fragmento [ConfigFragment], mais concretamente nos quatro métodos:

  • [saveFragment]: deve guardar o estado do fragmento para que possa ser restaurado posteriormente;
  • [initFragment]: que deve inicializar determinados campos do fragmento, se necessário. Este método é chamado quando a aplicação é iniciada e sempre que o dispositivo é rodado. Mais precisamente, é chamado quando o fragmento se torna visível após um dos dois eventos anteriores;
  • [initView]: que deve inicializar determinados componentes da vista, se necessário. Este método é chamado sempre que [initFragment] for chamado e quando a vista tiver de ser redesenhada porque o fragmento, em algum momento, saiu da vizinhança do fragmento exibido. Tal como anteriormente, é chamado quando o fragmento se torna visível após um destes eventos;
  • [updateOnRestore]: que é executado após os dois métodos anteriores quando o dispositivo foi rodado, mas também quando ocorreu uma navegação. A sua função é restaurar o estado anterior do fragmento;

Estes métodos serão os seguintes:


// arduinos 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) {
    // adapter listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // listview / adapter connection
    listArduinos.setAdapter(adapterListArduinos);
    // 1st visit?
    if (previousState == null) {
      // ListView empty - made by [initFragment]
      // hidden error message
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    } else {
      // error message visibility is restored
      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();
}
  • linha 2: o adaptador ListView para os Arduinos. É uma variável global porque é utilizada em diferentes métodos;
  • linhas 7–12: o método [saveFragment] guarda a visibilidade do TextView txtMsgErreurUrlServiceRestVisible (linha 10) num tipo [ConfigFragmentState];
  • linhas 14–19: o método [initFragment] inicializa o adaptador da linha 2 com a lista de Arduinos atualmente na sessão (linha 17). Note-se que a função de [initFragment] é inicializar os campos do fragmento. Aqui, esta inicialização deve ser realizada em todos os casos, quer seja a primeira visita (previousState == null) ou não;
  • linha 17: vemos que o adaptador está vinculado à fonte de dados [session.getCheckedArduinos]. Esta não deve ter o valor null. Por este motivo, o campo [session.checkedArduinos] é inicializado com uma lista vazia na sessão:

  // la liste des Arduinos
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
  • linhas 21–35: o método [initView] é responsável por inicializar determinados componentes da interface visual, particularmente aqueles cujos valores não são preservados quando o dispositivo é rodado;
  • linha 24: o Arduino ListView é vinculado ao adaptador da linha 2;
  • linhas 28–32: a primeira visita é distinguida das outras visitas;
  • linha 29: na primeira visita, deve ser exibido um [ListView] vazio. Isto acontece porque, na primeira visita, o adaptador [ListView] estava associado a uma lista vazia (linha 17);
  • linha 31: a mensagem de erro é ocultada;
  • linhas 32–36: o caso em que esta não é a primeira visita;
  • o [ListView] já se encontra no estado correto a partir da linha 24. Não há mais nada a fazer;
  • linhas 34–35: a mensagem de erro é restaurada para o estado em que se encontrava quando o fragmento foi guardado pela última vez;
  • linhas 31–36: o método [updateOnRestore] deve restaurar o fragmento ao seu estado inicial. Chegamos ao método [updateOnRestore] de duas maneiras:
    • ou porque o dispositivo foi rodado. Neste caso, todas as inicializações necessárias já foram realizadas em [initView];
    • ou porque estamos a navegar de um separador para o separador [Config]. Se o fragmento [Config] tiver saído da vizinhança dos fragmentos exibidos desde que o deixámos, o método [initView] já foi executado e o fragmento já se encontra no estado desejado. Se o fragmento [Config] não tiver saído da lista de fragmentos exibidos desde que foi abandonado, os seus componentes visuais não alteraram de estado e não há nada a fazer;

Vemos que o método [updateOnRestore] não tem nada para fazer. Às vezes é assim, outras vezes não. A diferença vem do método [updateOnSubmit]: se este método fizer algo que torne desnecessárias certas inicializações feitas em [initView], então essas inicializações devem ser feitas no método [updateOnRestore]. Tomemos o exemplo de um botão de opção com três valores: V1, V2 e V3. Talvez, no caso de navegação associada a uma ação [SUBMIT], o botão de opção selecionado deva ser sempre aquele com o valor V1. Neste caso, restaurar o valor do botão de opção no método [initView] é desnecessário, porque no caso de um [SUBMIT], este valor será substituído pelo fornecido pelo método [updateOnSubmit]. É, portanto, preferível mover esta restauração para o método [updateOnRestore] para evitar realizar uma operação desnecessária.

  • linhas 48–52: o método [notifyEndOfUpdates] é executado após todos os anteriores;
  • Linha 51: Os botões são definidos para o seu estado inicial: o botão [Refresh] é exibido e o botão [Cancel] é ocultado:

Tarefa: Adicione o código acima ao [ConfigFragment] e, em seguida, execute a aplicação. Repare que, quando roda o dispositivo, o separador [Config] mantém o seu estado (mensagem de erro, lista de Arduinos). Verifique se o mesmo comportamento ocorre quando simplesmente navega do separador [Config] para o separador [Commands] --> separador [Config]. Neste último caso, se tiver definido a adjacência do fragmento como 1 em [IMainActivity], a vista [ConfigFragment] é destruída ao mudar para o separador [Commands] e recriada ao regressar ao separador [Config]. Durante os testes, examine os registos.


5.6.10.8. Melhoria do código

O código do fragmento [ConfigFragment] pode ser melhorado. Por exemplo, escrevemos:


// arduinos list adapter
  private ListArduinosAdapter adapterListArduinos;
 
...
 
  // arduinos list display
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // display Arduinos
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
  }
 
  // raz list of Arduinos
  private void clearArduinos() {
    // an empty list is displayed
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
  • Podemos ver que nas linhas 9 e 16, estamos a utilizar uma variável local que está desligada do campo na linha 2, apesar de estarmos a tentar manipular a mesma entidade;

Atualizamos o código da seguinte forma:


  // arduinos list adapter
  private ListArduinosAdapter adapterListArduinos;
 
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  ...
  }
 
  private void getArduinosInBackground() {
 ...
    // it is consumed
    consumeArduinosResponse(response);
  }
 
  // response display
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // mistake?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // cancellation
      doAnnuler();
      // back to Ui
      return;
    }
    // we create a list of [CheckedArduino]
    List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
    checkedArduinos.clear();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // we display them
    adapterListArduinos.notifyDataSetChanged();
    // we cancel the wait
    cancelWaitingTasks();
}
 
  @Override
  protected void initFragment(CoreState previousState) {
    // adapt listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // listview / adapter connection
    listArduinos.setAdapter(adapterListArduinos);
    ...
}
  • Quando o método na linha 5 é executado, o ciclo de vida do fragmento já foi concluído. Portanto:
    • o adaptador na linha 2 foi associado à sua fonte de dados (linha 41);
    • o [ListView] dos Arduinos conectados foi vinculado a este adaptador (linha 48);

Quando quisermos alterar a exibição do [ListView], precisamos de fazer duas coisas:

  • alterar o conteúdo da fonte de dados [session.checkedArduinos];
  • notificar o adaptador desta alteração utilizando a instrução [adapterListArduinos.notifyDataSetChanged()];

É importante alterar o conteúdo da fonte de dados, e não a própria fonte de dados. Se alterarmos a própria fonte de dados, a operação [adapterListArduinos.notifyDataSetChanged()] continuará a exibir a fonte de dados antiga. Teríamos então de associar o adaptador à nova fonte de dados.

O código é o seguinte:

  • linha 27: recuperamos a fonte de dados;
  • linha 28: limpamos a fonte de dados. Por este motivo, removemos o método [clearArduinos];
  • linhas 29–31: adicionamos novos itens a esta lista agora vazia;
  • linha 33: dizemos ao adaptador para atualizar. Isto irá atualizar a exibição do [ListView] associado;

Tarefa: Efetue estas alterações e verifique se a sua aplicação continua a funcionar.


5.6.11. Comunicação entre vistas

Para verificar a comunicação entre as vistas, faremos com que todas as outras vistas exibam a lista de Arduinos obtida pela vista [Config]. Comecemos pela vista [blink.xml]. Enquanto anteriormente não exibia nada, agora irá exibir a lista de Arduinos ligados:

Image

 

O código XML para a vista [blink.xml] será o seguinte:


<?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/titre_list_arduinos"
      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 foi retirado diretamente da visualização [config.xml]. Simplesmente modificámos a margem superior na linha 19.


Tarefa: Duplique este código nas vistas [commands.xml, pinread.xml, pinwrite.xml].


O código do fragmento [BlinkFragment] associado à vista [blink.xml] também está a mudar:

  

  // visual components
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
 
  // arduinos list adapter
  private ListArduinosAdapter adapterListArduinos;
...
 
  // methods imposed by the parent class -------------------------------------------------------
 
...
  @Override
  protected void initFragment(CoreState previousState) {
    // adapter listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), true);
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // listview / adapter connection
    listArduinos.setAdapter(adapterListArduinos);
  }
...
  • linhas 2-3: o componente [ListView] para os Arduinos ligados;
  • linha 6: o adaptador para este [ListView];
  • linhas 12-23: o código para os métodos [initFragment] e [initView] é o mesmo que já foi utilizado para o fragmento [ConfigFragment];
  • linha 15: quando o fragmento precisa de ser reiniciado, reiniciamos o adaptador da linha 2, associando-o à lista de Arduinos armazenada na sessão. O último parâmetro [true] do construtor [ListArduinosAdapter] significa que queremos ver uma caixa de seleção ao lado de cada Arduino;
  • linha 22: quando a vista do fragmento precisa de ser reiniciada, associamos o [ListView] dos Arduinos ligados ao adaptador da linha 6;

Tarefa: Duplique este código nos outros fragmentos [CommandsFragment, PinReadFragment, PinWriteFragment]. Execute a aplicação e verifique se cada separador apresenta agora a lista de Arduinos ligados. Verifique também se, ao marcar Arduinos num separador e navegar para outro separador, estes permanecem marcados neste último.


Nota: A razão pela qual os Arduinos permanecem marcados é a seguinte. A classe [ListArduinosAdapter] foi apresentada na Secção 5.6.10.4. O código relacionado com a caixa de seleção é o seguinte:


        // 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) {
            // we assign its value
            ck.setChecked(arduino.isChecked());
            // we manage the click
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
}
  • Linhas 11–15: Se uma caixa de seleção estiver marcada na guia X, a propriedade [checked] do Arduino na linha 2 é definida como true (linha 14);
  • ao mudar para o separador Y, é apresentada a [ListView] dos Arduinos nesse separador. Na linha 9, vemos que se o Arduino na linha 2 tiver a sua propriedade [checked] definida como true, então a caixa de seleção [ck] na linha 5 será marcada;

5.6.12. A camada [DAO]

Nota: Para esta secção, reveja a implementação da camada [DAO] no projeto [example-16B] (ver secção 2.8.3).

Até agora, gerámos manualmente a lista de Arduinos ligados. Vamos agora solicitá-la ao servidor web / jSON. Para tal, vamos construir a camada [DAO]:

  

5.6.12.1. A interface IDao

A interface [IDao] da camada [DAO] será a seguinte:


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 setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // spécifique ----------------------------------------
  // list of arduinos
  Observable<Response<List<Arduino>>> getArduinos();
}
  • linhas 11-26: estas linhas já estão presentes na interface [IDao] do projeto modelo [client-android-skel];
  • linha 30: o método [getArduinos] devolve a lista de Arduinos ligados como um observável do tipo Observable<[Response<List<Arduino>>>] ;

Note que [Response<T>] é o tipo de todas as respostas enviadas pelo servidor na forma de uma cadeia 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;
    // the body of the reply
    private T body;
 
    // manufacturers
    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. A interface [WebClient]

  

A interface [WebClient] é uma interface para a qual a biblioteca AA fornece uma implementação. Esta interface será a seguinte:


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);
 
  // spécifique --------------------------------------
  // list of arduinos
  @Get("/arduinos")
  Response<List<Arduino>> getArduinos();
}
  • linhas 15-19: estas linhas estão incluídas por predefinição na interface [WebClient] do projeto modelo [client-android-skel];
  • linha 23: a URL do servidor utilizada para recuperar a lista de Arduinos através de um pedido GET. Note-se que esta URL é relativa à URL raiz [RestClientRootUrl] na linha 16;
  • linha 24: o servidor devolve uma cadeia JSON do tipo [Response<List<Arduino>>]. Esta cadeia JSON é automaticamente deserializada para o tipo [Response<List<Arduino>>] utilizando o conversor JSON [MappingJackson2HttpMessageConverter] da linha 15;

5.6.12.3. A classe [Dao]

A classe [Dao] implementa a interface [IDao] da seguinte forma:


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 customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // we build the restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // set the URL of the web service
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String mdp) {
    // the user is registered in the interceptor
    authInterceptor.setUser(user, mdp);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // factory configuration
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }
 
  // méthodes privées -------------------------------------------------
  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();
      }
    });
  }
}
  • linhas 19–87: estas linhas fazem parte da classe [Dao] no projeto [client-android-skel];
  • linhas 91–100: implementação do método [getArduinos];
  • linha 94: o método [getResponse] da classe pai é chamado. O único parâmetro deste método é uma instância da interface [IRequest<T>];
  • linhas 95–99: o único método da interface [IRequest<T>] é o método [T getResponse()];
  • linha 94: o tipo T de [IRequest<T>] deve ser o tipo T do resultado Observable<T> do método na linha 92, portanto, aqui, um tipo [Response<List<Arduino>>];
  • linha 97: o método [IRequest.getResponse()] delega o trabalho ao método [webClient.getArduinos()] que introduzimos. [webClient], definido na linha 24, é instanciado pela biblioteca AA e é uma instância da interface [WebClient] que introduzimos;

5.6.13. O [MainActivity]

  

Já apresentámos a atividade [MainActivity] na Secção 5.6.8. Ela estende a classe [AbstractActivity] e, como tal, implementa a interface [IMainActivity], que por sua vez estende a interface [IDao]. Sempre que um método é adicionado à interface [IDao], este deve ser implementado na classe [MainActivity]. O método [IDao.getArduinos] adicionado à interface [IDao] será implementado da seguinte forma na [MainActivity]:


...
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
...
 
  // implémentation IDao -----------------------------------------
  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    return dao.getArduinos();
  }
}
  • linhas 15–18: o método [getArduinos] é implementado delegando o trabalho à classe [Dao] que acabámos de apresentar e à qual temos uma referência na linha 8;

5.6.14. O fragmento [ConfigFragment] revisitado

Na classe [ConfigFragment], o código executado quando o botão [Refresh] é clicado é atualmente o seguinte:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    ...
    // request the list of Arduinos running in the background
    getArduinosInBackground();
  }
 
  private void getArduinosInBackground() {
    // create a fictitious arduino list
    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));
    }
    // we simulate a server response
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // it is consumed
    consumeArduinosResponse(response);
  }
 
  // response display
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    ...
}

Precisamos de reescrever as linhas 10–16, que codificavam de forma rígida uma resposta do tipo [Response<List<Arduino>>]. Agora, precisamos de solicitar esta lista à camada [DAO] através da atividade. O código fica assim:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // check entries
    if (!pageValid()) {
      return;
    }
    // save input
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // we prepare to wait
    beginWaiting(1);
    // the asynchronous task is executed
    executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {
 
      @Override
      public void call(Response<List<Arduino>> response) {
        // we consume the answer
        consumeArduinosResponse(response);
      }
    });
}
  • linha 8: a URL raiz do serviço web / JSON introduzida pelo utilizador é passada para a camada [DAO] através da atividade. Esta será a URL raiz da interface [WebClient] (ver secção 5.6.12.2);
  • linha 10: a classe pai é notificada de que uma tarefa assíncrona está prestes a ser iniciada;
  • linhas 12–19: lançamento da tarefa assíncrona que irá devolver a lista de Arduinos ligados ao servidor;
  • linha 12: chamar o método [executeInBackground] da classe pai. Este método espera dois parâmetros:
    • linha 12: o processo a observar. Este processo é fornecido aqui pelo método [mainActivity.getArduinos()];
    • linhas 12–19: uma instância da interface [Action1<T>], onde o tipo T é o tipo fornecido pelo processo, aqui um tipo [Response<List<Arduino>>];
  • linhas 14–18: o método chamado quando a tarefa assíncrona retorna o seu resultado do tipo [Response<List<Arduino>>];
  • linha 17: a resposta recebida é passada para o método [consumeArduinosResponse] definido anteriormente;

Tarefa: Inicie o servidor conforme descrito na Secção 5.4. Ligue um ou mais Arduinos ao PC no qual o servidor está a ser executado. Em seguida, inicie o cliente Android e verifique se consegue recuperar com sucesso a lista de Arduinos ligados. Observe os registos.


Image

  • Introduza o URL indicado em [1]. Este é um dos endereços IP do seu servidor;
  • Clique no botão [2];
  • Deve ver a lista de Arduinos ligados em [3];

Verifique se esta lista também aparece nos outros separadores.

5.7. Próximos passos


Seguindo o mesmo procedimento utilizado para a vista [Config], implemente e, em seguida, teste as outras quatro vistas da aplicação, uma a uma: [Blink], [PinRead], [PinWrite] e [Commands].


As vistas a criar foram apresentadas na Secção 5.5.

Para cada vista, deve:

  • desenhar a vista XML (ver Secção 5.6.9);
  • construir o fragmento associado (ver Secção 5.6.10);
  • adicionar um método à interface [WebClient] (ver Secção 5.6.12.2);
  • adicionar um método à interface [IDao] (ver Secção 5.6.12.2);
  • adicionar um método à classe [Dao] (ver secção 5.6.12.3);
  • adicionar um método à atividade [MainActivity] (ver secção 5.6.13);
  • escrever os manipuladores de eventos do fragmento (ver secção 5.6.14);
  • testar e observar os registos;

Nota 1: O exemplo a seguir é o projeto [Example-16B] do curso (ver secção 2.8.3).

Nota 2: Os URLs a consultar e o tipo das respetivas respostas foram apresentados na secção 5.4.2.

Nota 3:

A classe [CommandsFragment] envia uma lista contendo um único comando a ser executado por um ou mais Arduinos. Este comando será encapsulado na seguinte classe [ArduinoCommand]:


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

Na interface [WebClient], o método para executar esta lista de comandos será o seguinte:


  // envoi de commandes JSON
  @Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
  • linha 2: o URL é solicitado com uma solicitação HTTP POST;
  • linha 3: o valor enviado deve ter a anotação [@Body];

Nota 4: Recomenda-se abordar esta tarefa da seguinte forma:

  • só avance para a próxima vista depois de a vista atual ter sido criada e testada;
  • gerir o estado das vistas apenas após obter uma aplicação funcional em condições normais. Em seguida, para cada vista, percorra o dispositivo para diferentes estados da vista e anote qualquer informação perdida. Estes são os dados que precisam de ser guardados e posteriormente restaurados. A seguir, verifique a navegação: quando sair de um separador e regressar a ele mais tarde, este deve estar no mesmo estado em que o deixou;