Skip to content

5. Compito 2 - Controllo di Arduino con un tablet Android

Ora impareremo come controllare una scheda Arduino con un tablet. L'esempio da seguire è il progetto [client-android-skel] del corso (vedi paragrafo 2).

5.1. Architettura del progetto

L'intero progetto avrà la seguente architettura:

  • Il blocco [1], ovvero il server web/JSON e gli Arduino, vi sarà fornito;
  • dovrete realizzare il blocco [2], il programma per tablet Android per comunicare con il server web / JSON.

5.2. Hardware

Sono a vostra disposizione i seguenti componenti:

  • un Arduino con uno shield Ethernet, un LED e un sensore di temperatura;
  • un miniHub da condividere con un altro studente;
  • un cavo USB per alimentare l'Arduino;
  • due cavi di rete per collegare l'Arduino e il PC alla stessa rete privata;
  • un tablet Android;

5.2.1. L'Arduino

Ecco come collegare tra loro i vari componenti:

  • scollegare il cavo di rete dal PC;
  • collegare il PC e l'Arduino utilizzando un cavo di rete;
  • L'Arduino in vostro possesso sarà già programmato. Il suo indirizzo IP sarà [192.168.2.2]. Affinché il PC riconosca l'Arduino, è necessario assegnargli un indirizzo IP sulla rete [192.168.2]. Gli Arduino sono stati programmati per comunicare con un PC con l'indirizzo IP [192.168.2.1]. Ecco come procedere:

Vai su [Pannello di controllo\Rete e Internet\Centro connessioni di rete e condivisione]:

 
  • In [1], fare clic sul collegamento [Rete locale];
  • in [2], fare clic sul pulsante [Proprietà] della rete locale;
  • in [3], fare clic sulle proprietà [IPv4] della scheda [Connessione alla rete locale];
  • in [4], assegnare a questa scheda l'indirizzo IP [192.168.2.1] e la subnet mask [255.255.255.0];
  • in [5], fare clic su [OK] tutte le volte necessarie per uscire dalla procedura guidata.

5.2.2. Il tablet

  • Utilizzando la scheda Wi-Fi, connetti il tuo computer alla rete Wi-Fi che ti forniremo. Fai lo stesso con il tuo tablet;
  • Controlla l'indirizzo IP Wi-Fi del tuo PC digitando [ipconfig] in una finestra del Prompt dei comandi. Troverai un indirizzo simile a [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
  • Controlla l'indirizzo IP Wi-Fi del tuo tablet. Se non sei sicuro di come fare, chiedi al tuo istruttore. Troverai un indirizzo simile a [192.168.x.z];
  • Disattiva il firewall del PC se è attivo [Pannello di controllo\Sistema e sicurezza\Windows Firewall];
  • In una finestra del Prompt dei comandi, verifica che il PC e il tablet possano comunicare digitando il comando [ping 192.168.x.z], dove [192.168.x.z] è l'indirizzo IP del tuo tablet. Il tablet dovrebbe quindi rispondere:
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

La configurazione di rete del tuo sistema è ora pronta.

5.2.3. L'emulatore [Genymotion]

L'emulatore [Genymotion] (vedi Sezione 6.9) è un'ottima alternativa al tablet. È veloce quasi quanto un tablet e non richiede una connessione Wi-Fi. Ti consigliamo di utilizzare questo metodo. Puoi usare il tablet per il test finale della tua app.

5.3. Programmazione di Arduino

Qui ci concentriamo sulla scrittura di codice C per Arduino:

Vedi anche

  • Installazione dell'IDE di sviluppo Arduino (vedere la Sezione 6.1);
  • Utilizzo delle librerie JSON (Appendici, Sezione 6.6);
  • Nell'IDE di Arduino, prova l'esempio di un server TCP (ad es. il server web) e quello di un client TCP (ad es. il client Telnet);
  • le appendici sull'ambiente di programmazione Arduino nella sezione 6.1.

Un Arduino è un insieme di pin collegati all'hardware. Questi pin sono ingressi o uscite. I loro valori sono binari o analogici. Per controllare l'Arduino, esistono due operazioni di base:

  • scrivere un valore binario/analogico su un pin identificato dal suo numero;
  • leggere un valore binario/analogico da un pin identificato dal suo numero;

A queste due operazioni di base ne aggiungeremo una terza:

  • far lampeggiare un LED per una certa durata e a una certa frequenza. Questa operazione può essere eseguita richiamando ripetutamente le due operazioni di base precedenti. Tuttavia, vedremo nei test che gli scambi tra il livello [DAO] e un Arduino richiedono dell'ordine di un secondo. Non è quindi possibile far lampeggiare un LED ogni 100 millisecondi, ad esempio. Implementeremo quindi questa funzione di lampeggiamento sull'Arduino stesso.

L'Arduino funzionerà come segue:

  • La comunicazione tra il livello [DAO] e un Arduino avviene tramite una rete TCP-IP attraverso lo scambio di righe di testo in formato JSON (JavaScript Object Notation);
  • all'avvio, l'Arduino si connette alla porta 100 di un server di registrazione situato nel livello [DAO]. Invia una singola riga di testo al server:
{"id":"cuisine","desc":"duemilanove","mac":"90:A2:DA:00:1D:A7","port":102}

Si tratta di una stringa JSON che descrive l'Arduino che si sta connettendo:

  • id: un identificatore per l'Arduino;
  • desc: una descrizione di ciò che l'Arduino è in grado di fare. In questo caso, abbiamo semplicemente specificato il modello dell'Arduino;
  • mac: l'indirizzo MAC dell'Arduino;
  • porta: il numero di porta su cui Arduino attenderà i comandi provenienti dal livello [DAO].

Tutte queste informazioni sono in formato stringa, tranne la porta, che è un numero intero.

  • Una volta che Arduino si è registrato con il server di registrazione, inizia ad ascoltare sulla porta specificata al server (102 sopra). Attende comandi JSON nel seguente formato:
{"id":"identifiant","ac":"une_action","pa":{"param1":"valeur1","param2":"valeur2",...}}

Si tratta di una stringa JSON con i seguenti elementi:

  • id: un identificatore per il comando. Può essere qualsiasi cosa;
  • ac: un'azione. Ce ne sono tre:
  • pw (scrittura su pin) per scrivere un valore su un pin,
  • pr (lettura pin) per leggere il valore da un pin,
  • cl (blink) per far lampeggiare un LED;
  • pa: i parametri dell'azione. Questi dipendono dall'azione.
  • Arduino restituisce sempre una risposta al proprio client. Si tratta di una stringa JSON nel seguente formato:
{"id":"1","er":"0","et":{"pinx":"valx"}}

dove

  • id: l'identificatore del comando a cui si sta rispondendo;
  • er (errore): un codice di errore se si è verificato un errore, altrimenti 0;
  • et (status): un dizionario che è sempre vuoto tranne che per il comando di lettura pr. Il dizionario contiene quindi il valore del pin numero x che è stato richiesto.

Ecco alcuni esempi per chiarire le specifiche precedenti:

Fai lampeggiare il LED n. 8 10 volte con un intervallo di 100 millisecondi:

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

I parametri del comando cl sono: la durata (dur) di un lampeggio in millisecondi, il numero (nb) di lampeggi e il numero del pin del LED.

Scrivi il valore binario 1 sul pin 7:

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

I parametri pa del comando pw sono: la modalità di scrittura mod (b per binario o a per analogico), il valore val da scrivere e il numero del pin. Per una scrittura binaria, val è 0 o 1. Per una scrittura analogica, val è compreso nell'intervallo [0,255].

Scrivi il valore analogico 120 sul pin 2:

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

Leggi il valore analogico dal pin 0:

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

I parametri pa del comando pr sono: la modalità di lettura (binaria o analogica) e il numero del pin. Se non ci sono errori, Arduino inserisce il valore del pin richiesto nella chiave "et" della sua risposta. In questo caso, pin0 indica che è stato richiesto il valore del pin 0, e 1023 è quel valore. In modalità di lettura, un valore analogico sarà compreso nell'intervallo [0, 1024].

Abbiamo presentato i tre comandi cl, pw e pr. Ci si potrebbe chiedere perché non abbiamo utilizzato campi più espliciti nelle stringhe JSON, come action invece di ac, pinwrite invece di pw e parameters invece di pa. Un Arduino ha una memoria molto limitata. Tuttavia, le stringhe JSON scambiate con l’Arduino incidono sull’utilizzo della memoria. Abbiamo quindi scelto di accorciarle il più possibile.

Ora esaminiamo alcuni casi di errore:

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

È stato inviato un comando che non è in formato JSON. Arduino ha restituito il codice di errore 100.

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

È stato inviato un comando pr senza il parametro pin. Arduino ha restituito il codice di errore 302.

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

Abbiamo inviato un comando pinread sconosciuto (è pr). Arduino ha restituito il codice di errore 104.

Non proseguiremo con gli esempi. La regola è semplice. L’Arduino non deve andare in crash, indipendentemente dal comando che gli viene inviato. Prima di eseguire un comando JSON, si assicura che il comando sia valido. Non appena si verifica un errore, l’Arduino interrompe l’esecuzione del comando e restituisce la stringa di errore JSON al proprio client. Ancora una volta, poiché lo spazio di memoria è limitato, restituiamo un codice di errore anziché un messaggio completo.

Il codice del programma in esecuzione su Arduino è fornito negli esempi in questo documento:

  

Per trasferirlo su Arduino:

  • Collegalo al PC;
  • in [1], apri il file [arduino_uno.ino]. L'IDE di Arduino si avvierà e caricherà il file;

Nota: il codice è stato originariamente creato e testato con l'IDE di Arduino 1.5.x. Da allora sono state rilasciate altre versioni dell'IDE. Il codice non ha funzionato con l'IDE di Arduino 1.6.x. Sembra che vi sia un problema di retrocompatibilità tra le versioni 1.6 e 1.5.

  • In [2-4], specificare il tipo di Arduino utilizzato;
  • In [5-7], specificare a quale porta seriale del PC è collegato;
  • in [8], caricare il programma [arduino_uno] su Arduino;

Il codice del programma è ampiamente commentato. I lettori interessati possono consultarlo. Ci limitiamo a evidenziare le righe di codice che configurano la comunicazione bidirezionale client/server tra l'Arduino e il 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()
{
  ...
}
  • Riga 8: l'indirizzo MAC dell'Arduino. In questo caso non ha molta importanza, poiché l'Arduino si troverà su una rete privata con un PC e uno o più Arduino. L'indirizzo MAC deve semplicemente essere unico su questa rete privata. Normalmente, la scheda di rete dell'Arduino ha un adesivo che indica l'indirizzo MAC della scheda. Se questo adesivo manca e non conosci l'indirizzo MAC della scheda, puoi inserire qualsiasi valore nella riga 8 purché venga rispettata la regola dell'unicità dell'indirizzo MAC sulla rete privata;
  • riga 11: l'indirizzo IP della scheda. Anche in questo caso, è possibile inserire qualsiasi valore del tipo [192.168.2.x] e variare la x per i diversi Arduino presenti sulla rete privata;
  • Riga 13: identificatore Arduino. Deve essere unico tra gli identificatori degli Arduino sulla stessa rete privata;
  • riga 15: la porta di servizio dell'Arduino. È possibile inserire qualsiasi valore;
  • riga 17: descrizione della funzione dell'Arduino. È possibile inserire qualsiasi valore. Prestare attenzione alle stringhe lunghe a causa della memoria limitata dell'Arduino;
  • riga 21: indirizzo IP del server di registrazione di Arduino sul PC. Non deve essere modificato;
  • riga 23: porta per questo servizio di registrazione. Non deve essere modificata;

5.4. Il server Web/JSON

5.4.1. Installazione

Image

Viene fornito il binario Java per il server Web/JSON:

 

Apri un prompt dei comandi e digita il seguente comando:

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

Se [java.exe] non è presente nel PATH del prompt dei comandi, dovrai digitare il percorso completo di [java.exe] (di solito C:\Program Files\java\...).

Si aprirà una finestra DOS che visualizzerà i log:


.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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
  • riga 11: viene avviato un server Tomcat incorporato;
  • riga 15: il servlet Spring MVC [dispatcherServlet] viene caricato ed eseguito;
  • riga 18: viene rilevato l'URL REST [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}];
  • riga 19: viene rilevato l'URL REST [/arduinos/commands/{idArduino}];
  • riga 20: viene rilevato l'URL REST [/arduinos/];
  • riga 21: viene rilevato l'URL REST [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}];
  • riga 22: viene rilevato l'URL REST [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}];
  • riga 26: viene avviato il server di registrazione Arduino;

Collega il tuo Arduino al PC, se non l'hai già fatto. Il firewall del PC deve essere disabilitato. Quindi, in un browser web, inserisci l'URL [http://localhost:8080/arduinos]:

Dovresti vedere apparire l'ID dell'Arduino collegato. Se non appare nulla, prova a resettare l'Arduino. È dotato di un pulsante di reset proprio a questo scopo.

Il server web/JSON è ora installato.

5.4.2. Gli URL esposti dal servizio web/JSON

Vedi: progetto [Esempio-15] (vedi sezione 1.16.1);

Il servizio web/JSON è stato implementato utilizzando Spring MVC ed espone i seguenti URL:


@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 {
  ...
  }
}

Le risposte inviate dal server sono rappresentazioni JSON della seguente 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
...
}

L'URL [/arduinos] restituisce una risposta di tipo [Response<List<Arduino>>], dove [Arduino] è la seguente 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
...
}
  • riga 7: [id] è l'identificatore di Arduino;
  • riga 8: la sua descrizione;
  • riga 9: il suo indirizzo MAC;
  • riga 10: il suo indirizzo IP;
  • riga 11: la porta su cui è in ascolto per i comandi;

Gli URL:

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

restituisce una risposta di tipo [Response<ArduinoResponse>], dove la classe [ArduinoResponse] rappresenta la risposta standard di Arduino:


public class ArduinoResponse implements Serializable {
 
  private String json;
  private String id;
  private String erreur;
  private Map<String, Object> etat;
 
  // getters and setters
...
}
  • [json]: la stringa JSON inviata da un Arduino che non è stato possibile decodificare (in caso di errore), null in caso contrario;
  • [id]: l'identificatore del comando a cui Arduino sta rispondendo;
  • [error]: un codice di errore, 0 se tutto OK, altrimenti un altro valore;
  • [status]: un dizionario contenente la risposta specifica per il comando. Di solito è vuoto, a meno che il comando non abbia richiesto la lettura di un valore da Arduino; in tal caso, tale valore verrà inserito in questo dizionario;

5.4.3. Test del servizio web / JSON

Prendi familiarità con il server web / JSON testando i seguenti URL:

URL
ruolo
http://localhost:8080/arduinos/
Restituisce l'elenco degli Arduino collegati
http://localhost:8080/arduinos/
blink/1/cucina/8/100/20/
accende il LED sul pin 8
 su Arduino identificato come "cuisine",
 20 volte ogni 100 ms.
http://localhost:8080/arduinos/
pinRead/1/cuisine/0/a/
lettura analogica dal pin 0 di
 Arduino identificato come "cucina"
http://localhost:8080/arduinos/
pinRead/1/cucina/5/b/
lettura binaria del pin 5 di
 Arduino identificato da cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/8/b/1/
Scrivi il valore binario 1 sul pin 8 dell'Arduino identificato da
 cuisine
http://localhost:8080/arduinos/
pinWrite/1/kitchen/4/a/100/
Scrittura analogica del valore 100 sul pin 4 dell'Arduino identificato
 come "cuisine"

Ecco alcuni screenshot di ciò che dovresti vedere:

Ottieni l'elenco degli Arduino collegati:

La stringa JSON ricevuta dal server web / JSON è un oggetto con i seguenti campi:

  • [status]: 0 indica che non si è verificato alcun errore; in caso contrario, si è verificato un errore;
  • [messages]: un elenco di messaggi che spiegano l'errore, se si è verificato un errore:
  • [body]: l'elenco degli Arduino se non si è verificato alcun errore. Ciascun Arduino è quindi descritto da un oggetto con i seguenti campi:
    • [id]: l'identificatore dell'Arduino. Non possono esserci due Arduino con lo stesso identificatore;
    • [description]: una breve descrizione delle funzionalità dell'Arduino;
    • [mac]: l'indirizzo MAC dell'Arduino;
    • [ip]: l'indirizzo IP dell'Arduino;
    • [port]: la porta su cui è in ascolto per i comandi;

Fai lampeggiare 20 volte ogni 100 ms il LED sul pin 8 di Arduino identificato da [cuisine]:

 

La stringa JSON ricevuta dal server web / JSON è un oggetto con i seguenti campi:

  • [status]: 0 indica che non si è verificato alcun errore; in caso contrario, si è verificato un errore;
  • [messages]: un elenco di messaggi che spiegano l'errore, se si è verificato un errore:
  • [body]: la risposta di Arduino se non si è verificato alcun errore:
    • [id]: identificatore del comando. Questo identificatore è l'1 in [/blink/1]. Arduino include questo identificatore di comando nella sua risposta;
    • [error]: un numero di errore. Un valore diverso da 0 indica un errore;
    • [state]: utilizzato solo per la lettura di un pin. Il suo valore è il valore del pin;
    • [json]: utilizzato solo in caso di errore JSON tra il client e il server. Il suo valore è la stringa JSON errata inviata da Arduino;

Lettura analogica del pin 0 su Arduino identificato da [kitchen]:

 

La stringa JSON ricevuta dal server web /json è simile alla precedente, ad eccezione del campo [state], che rappresenta il valore del pin 0.

Lettura binaria del pin 5 su Arduino identificato da [kitchen]:

 

La stringa JSON ricevuta dal server web /json è simile alla precedente.

Scrittura binaria del valore 1 sul pin 8 dell'Arduino identificato da [kitchen]:

 

La stringa JSON ricevuta dal server web /json è simile alla precedente.

Testare l'URL [http://localhost:8080/arduinos/commands/cuisine] è più complicato. Il metodo /json del server web che gestisce questo URL richiede una richiesta POST, che non può essere facilmente simulata utilizzando un browser. Per testare questo URL, è possibile utilizzare un browser Chrome con l'estensione [Advanced REST Client] (vedere la sezione 6.13):

 
  • in [1], l'URL del metodo web/JSON da testare;
  • in [2], il metodo POST per inviare la richiesta;
  • in [3-4], il valore inviato è in formato JSON;
  • in [5], la stringa JSON che viene inviata. Notare le parentesi quadre che aprono e chiudono l'elenco. Qui, l'elenco contiene un solo comando JSON che fa lampeggiare il pin 8 10 volte ogni 100 ms;
  • in [6], la richiesta viene inviata;
 
  • in [7], la risposta JSON inviata dal server. L'oggetto ha ricevuto un oggetto con i due campi usuali [status, messages] e un campo [body] il cui valore è l'elenco delle risposte di Arduino a ciascuno dei comandi JSON inviati.

Vediamo cosa succede quando inviamo un comando JSON con una sintassi errata all'Arduino:

Riceviamo quindi la seguente risposta:

 

Possiamo notare che nella risposta di Arduino il codice di errore è [104], a indicare che il comando [xx] non è stato riconosciuto.

5.5. Test del client Android

Di seguito è riportato il file eseguibile del client Android completato:

  

Usa il mouse per trascinare il file [app-debug.apk] qui sopra su un emulatore di tablet [GenyMotion]. Verrà quindi salvato ed eseguito. Avvia anche il server web/jSON se non l'hai già fatto. Collega l'Arduino al PC con un LED collegato ad esso. Il client Android ti permette di gestire gli Arduino da remoto. Mostra all'utente le seguenti schermate.

La scheda [CONFIG] consente di connettersi al server e recuperare l'elenco degli Arduino connessi:

Image

  • In [1], inserisci l'indirizzo IP [192.168.2.1] assegnato al tuo PC (vedi sezione 5.2).

La scheda [PINWRITE] consente di scrivere un valore su un pin di Arduino:

Image

Image

La scheda [PINREAD] consente di leggere il valore da un pin di Arduino:

Image

La scheda [BLINK] consente di far lampeggiare un LED di Arduino:

Image

La scheda [COMMAND] consente di inviare un comando JSON a un Arduino:

Image

5.6. Il client Android per il servizio web / JSON

Ora parleremo della scrittura del client Android.

5.6.1. Architettura del client

L'architettura del client Android sarà quella del progetto [Esempio-15] (vedere la sezione 1.16.2);

  • il livello [DAO] comunica con il server web/JSON;

Il client Android deve essere in grado di controllare più Arduino contemporaneamente. Ad esempio, vogliamo poter far lampeggiare due LED su due Arduino contemporaneamente, non uno dopo l’altro. Pertanto, il nostro client Android utilizzerà un’attività asincrona per ogni Arduino, e queste attività verranno eseguite in parallelo.

5.6.2. Il progetto client di Android Studio

Duplica il progetto [client-android-skel] (vedi sezione 2) nel progetto [client-arduinos-01] (se necessario, rivedi come duplicare un progetto Gradle nella sezione 1.15):

5.6.3. Le cinque viste XML

  

Ci saranno cinque viste XML:

  • [blink]: per far lampeggiare un LED Arduino. È associata al frammento [BlinkFragment];
  • [commands]: per inviare un comando JSON a un Arduino. È associata al frammento [CommandsFragment];
  • [config]: per configurare l'URL del servizio web/JSON e recuperare l'elenco iniziale degli Arduino connessi. È associata al frammento [ConfigFragment];
  • [pinread]: per leggere il valore binario o analogico di un pin di Arduino. È associato al frammento [PinReadFragment];
  • [pinwrite]: per scrivere un valore binario o analogico su un pin di Arduino. È associato al frammento [PinWriteFragment];

Per ora, queste cinque viste XML avranno tutte lo stesso contenuto vuoto:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
 
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
  </RelativeLayout>
</ScrollView>
  • La vista si trova all'interno di un contenitore [RelativeLayout] (righe 7–10), a sua volta contenuto in un contenitore [ScrollView] (righe 2–11). Ciò garantisce la possibilità di scorrere la vista qualora questa superi le dimensioni dello schermo di un tablet;

Compito: creare le cinque viste XML.


5.6.4. Il menu dei frammenti

Sappiamo che i frammenti in un progetto creato con [client-android-skel] devono essere associati a un menu, anche se è vuoto. In questo caso, l'app non avrà un menu. Il menu vuoto è già presente nel progetto;

  

5.6.5. I cinque elementi dell'app

 

Compito: duplicare il frammento [DummyFragment] nei cinque frammenti dell'applicazione, come mostrato in [2].


Il frammento [ConfigFragment] presenta la seguente struttura di base:


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

Sostituisci la riga 10 con la seguente riga:


@EFragment(R.layout.config)

Compito: fare lo stesso per gli altri quattro frammenti modificando l'attributo [@EFragment] della classe.


Fragment
View
ConfigFragment

R.layout.config
PinReadFragment

R.layout.pinread
PinWriteFragment

R.layout.pinwrite
CommandsFragment

R.layout.commands
BlinkFragment

R.layout.blink

5.6.6. Stati del frammento

Ogni frammento avrà uno stato.


Compito: duplicare la classe [DummyFragmentState] cinque volte per creare i cinque stati mostrati in [2].


5.6.7. Personalizzazione del progetto

 

Il pacchetto [architecture / custom] contiene gli elementi personalizzabili dell'architettura dell'applicazione.

5.6.7.1. L'interfaccia [IMainActivity]

L'interfaccia [IMainActivity] definisce ciò che i frammenti possono richiedere all'attività, nonché le costanti dell'applicazione. Questa interfaccia sarà la seguente:


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;
}
  • righe 25, 28, 31, 40: configurazione del livello [DAO]. Questa applicazione interroga un server web/JSON;
  • riga 37: questa applicazione dispone di schede;
  • riga 43: questa applicazione ha cinque frammenti;
  • righe 46–50: i numeri dei cinque frammenti;
  • riga 34: adiacenza dei frammenti. Lo sviluppatore può impostare qui un valore compreso nell'intervallo [1, FRAGMENTS_COUNT-1];

5.6.7.2. La classe [CoreState]

La classe [CoreState] è la classe padre degli stati dei frammenti:


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
...
}
  • righe 12–16: le classi di stato per i cinque frammenti devono essere dichiarate qui;

5.6.8. La classe [MainActivity]

  

La classe [MainActivity] sarà la seguente:


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 -----------------------------------------
}
  • righe 46–50: creazione delle cinque schede dell'applicazione;
  • riga 48: i titoli delle schede sono forniti dal metodo alle righe 63-79;
  • i cinque frammenti vengono istanziati alla riga 60. Grazie alle annotazioni AA, le classi dei frammenti sono quelle presentate in precedenza, con il suffisso di un trattino basso;
  • righe 63-79: viene definito un titolo per ciascun frammento. Questi titoli saranno recuperati dal file [res/values/strings.xml]
  

Il contenuto di [strings.xml] è il seguente:


<?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>

Compito: creare gli elementi sopra elencati e compilare il progetto. Non dovrebbero esserci errori.


Esegui il progetto. Dovresti vedere la seguente schermata sull'emulatore:

Image

Esamina i log che accompagnano la visualizzazione della prima schermata e segui i vari passaggi che sono stati eseguiti. Passa da una scheda all'altra e continua a seguire i log.

5.6.9. La vista XML [config]

La vista XML [config] avrà questo aspetto:

La vista sopra riportata è generata dal seguente codice 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>

La vista utilizza stringhe (android:text alle righe 15, 25, 37, 50, 61, 73) definite nel file [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>

La vista utilizza i colori (android:textColor alle righe 51 e 62) definiti nel file [res/values/colors]:

  

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

La vista utilizza le dimensioni (android:textSize alla riga 16) definite nel file [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>

Questa tecnica non è stata utilizzata per tutte le dimensioni. Tuttavia, è l'approccio consigliato. Consente di modificare le dimensioni in un unico punto.


Compito: creare gli elementi sopra indicati.


Esegui nuovamente il progetto. Dovresti vedere la seguente vista:

Image

5.6.10. Il frammento [ConfigFragment]

Per gestire la nuova vista [config], il codice del frammento [ConfigFragment] cambia come segue:


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);
  }
}
  • righe 23–32: gli elementi dell'interfaccia visiva;
  • righe 58–60: alla prima visita al frammento, il messaggio di errore viene nascosto;
  • righe 73–76: ogni volta che il frammento viene visualizzato, il pulsante [Annulla] viene nascosto (riga 82) e viene visualizzato il pulsante [Aggiorna] (righe 86–87). Infatti, in questa applicazione, un frammento non può essere visualizzato mentre è in corso un'operazione asincrona, e quindi il pulsante [Annulla] è visibile;

Compito: creare gli elementi sopra indicati.


Esegui questa nuova versione. La prima vista dovrebbe ora apparire così:

Image

5.6.10.1. Il pulsante [Aggiorna]

Per ora, gestiremo il clic sul pulsante [Refresh] come segue:


@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);
  }
  • righe 1-5: il metodo eseguito quando si clicca sul pulsante [Aggiorna];
  • riga 4: avviamo l'attesa;
  • riga 18: passiamo il numero di attività asincrone da avviare alla classe padre. Apparirà l'immagine di caricamento;
  • righe 20-21: questa attesa farà apparire il pulsante [Cancel], scomparire il pulsante [Refresh] e apparire l'immagine di caricamento. Non succede nient'altro. Tuttavia, l'utente può cliccare sul pulsante [Cancel]. Verrà quindi eseguito il metodo nelle righe 7-14;
  • riga 13: alla classe padre viene chiesto di annullare tutte le attività. La classe lo farà e a sua volta chiamerà il metodo nelle righe 25–29 per segnalare che tutte le attività sono state completate. Il parametro [runningTasksHaveBeenCanceled] avrà il valore true per indicare che le attività sono state annullate;
  • Righe 35–36: il pulsante [Annulla] scomparirà, mentre il pulsante [Aggiorna] riapparirà.

Compito: apportare queste modifiche e quindi eseguire il progetto. Verificare che il pulsante [Refresh] avvii l'attesa e che il pulsante [Cancel] la interrompa. Osservare i log.


5.6.10.2. Convalida dell'input

Nella versione precedente non abbiamo convalidato l'URL inserito. Per convalidarlo, aggiungiamo il seguente codice in [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;
  }
  • riga 2: l'URL inserito;
  • righe 7–9: prima di fare qualsiasi cosa, controlliamo la validità dell'input;
  • riga 19: recuperiamo l'URL inserito e aggiungiamo il prefisso [http://];
  • riga 22: proviamo a costruire un oggetto URI (Uniform Resource Identifier) con esso. Se l'URL inserito è sintatticamente errato, verrà generata un'eccezione;
  • righe 23–27: viene generata un'eccezione se l'URI è valido ma [host==null] e [port==-1]. Questo è uno scenario possibile;
  • riga 30: si è verificata un'eccezione. Viene visualizzato il messaggio di errore;
  • riga 32: restituiamo [false] per indicare che la pagina non è valida;
  • riga 35: non si sono verificati errori. Restituiamo [true] per indicare che la pagina è valida;

Compito: implementare la funzionalità sopra descritta.


Prova questa nuova versione e verifica che gli URL non validi vengano contrassegnati correttamente.

5.6.10.3. Visualizzazione dell'elenco degli Arduino

  

Le diverse viste dovranno visualizzare l'elenco degli Arduino collegati. A tal fine, definiremo diverse classi e una vista XML:

  • Un Arduino sarà rappresentato dalla classe [Arduino] [1];
  • la classe [CheckedArduino] [1] eredita dalla classe [Arduino], alla quale abbiamo aggiunto un valore booleano per indicare se l'Arduino è stato selezionato in un elenco;

La classe [Arduino] è quella già utilizzata dal server e presentata nella sezione 5.4.2. È la seguente:


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
...
}
  • riga 7: [id] è l'identificatore di Arduino;
  • riga 8: la sua descrizione;
  • riga 9: il suo indirizzo MAC;
  • riga 10: il suo indirizzo IP;
  • riga 11: la porta su cui è in ascolto per i comandi;

Questa classe corrisponde alla stringa JSON ricevuta dal server quando si richiede l'elenco degli Arduino connessi:

La classe [CheckedArduino] eredita dalla 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;
    }
 
}
  • Riga 3: la classe [CheckedArduino] eredita dalla classe [Arduino];
  • riga 6: aggiungiamo una variabile booleana che ci dirà se è stato selezionato un Arduino dall'elenco visualizzato;

In [ConfigFragment], simuleremo il recupero dell'elenco degli Arduino collegati.

  

  @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));
}
  • riga 2: la ListView che visualizza gli Arduino connessi al server;
  • riga 5: il metodo che recupera l'elenco degli Arduino connessi;
  • riga 11: comunichiamo alla classe padre che stiamo per avviare un'attività asincrona;
  • riga 12: cancelliamo l'elenco degli Arduino attualmente visualizzato;
  • riga 15: richiediamo l'elenco degli Arduino connessi come attività in background;
  • righe 23–28: il metodo che cancella l'elenco degli Arduino attualmente visualizzato;

Il metodo [getArduinosInBackground] è il seguente:


  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);
}
  • righe 3–6: creiamo un elenco di 20 Arduino;
  • righe 8-9: costruiamo la risposta di tipo [Response<List<Arduino>>] (sezione 5.4.2) che incapsulerà l'elenco di Arduino creato;
  • riga 11: annulla l'attesa;
  • riga 13: reimpostiamo i pulsanti al loro stato iniziale;
  • riga 15: consumare la risposta;

Il metodo [consumeArduinosResponse] è il seguente:


  // 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);
}
  • righe 4-11: controlliamo il codice di errore nella risposta inviata dal server:
  • riga 4: se il codice di errore non è zero;
  • riga 6: visualizza i messaggi memorizzati dal server nel campo [messages] della risposta;
  • riga 8: tornare all'interfaccia utente;
  • righe 11-16: se non ci sono stati errori, visualizza l'elenco degli Arduino ricevuti, dopo averlo convertito in un tipo List<CheckedArduino>;

Il metodo [showArduinos] è il seguente:


  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));
}

Compito: Apporta le modifiche sopra indicate ed esegui il tuo progetto.


Cliccando sul pulsante [Aggiorna] dovresti vedere la seguente schermata:

Image

L'input in [1] non viene utilizzato. È quindi possibile inserire qualsiasi valore purché rispetti il formato previsto.

5.6.10.4. Un modello per la visualizzazione di un Arduino

Attualmente, gli Arduino collegati vengono visualizzati nella vista [Config] come segue:

Image

Ora vogliamo visualizzarli come segue:

Image

  • in [1], una casella di controllo che consentirà di selezionare un Arduino. Questa casella di controllo sarà nascosta quando si desidera visualizzare un elenco di Arduino non selezionabili;
  • in [2], l'ID dell'Arduino;
  • in [3], la sua descrizione;

Quanto segue si basa sui concetti sviluppati nei progetti [esempio-19] e [esempio-19B] nella Sezione 1.20. Rivedili se necessario.

Per prima cosa, creiamo la vista che visualizzerà un elemento dall'elenco degli Arduino:

 

Il codice per la vista [listarduinos_item] sopra riportata è il seguente:


<?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>
  • righe 9–15: la casella di controllo;
  • righe 17-23: il testo [Id: ];
  • righe 25-33: qui verrà inserito l'ID Arduino;
  • righe 35-43: il testo [Descrizione: ];
  • righe 45-53: qui verrà inserita la descrizione di Arduino;

Questa vista utilizza il testo (righe 23, 32, 43) definito in [res/values/strings.xml]:


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

La vista utilizza anche un colore (righe 33, 53) definito in [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>

Il gestore di visualizzazione per un elemento nell'elenco Arduino

  

La classe [ListArduinosAdapter] è la classe chiamata dalla [ListView] per visualizzare ogni elemento dell'elenco Arduino. Il suo codice è il seguente:


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) {
...
    }
}
  • riga 18: il costruttore della classe accetta quattro parametri: l'attività attualmente in esecuzione, l'ID della vista da visualizzare per ciascun elemento nell'origine dati, l'origine dati che popola l'elenco e un valore booleano che indica se la casella di controllo associata a ciascun Arduino debba essere visualizzata o meno;
  • Righe 8–15: Queste quattro informazioni vengono memorizzate localmente;

Riga 29: il metodo [getView] è responsabile della generazione della vista n. [position] nella [ListView] e della gestione dei relativi eventi. Il codice è il seguente:


@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;
    }
  • riga 2: il primo parametro è la posizione nella [ListView] della riga da creare. È anche la posizione nell'elenco degli Arduino memorizzato localmente;
  • riga 4: recuperiamo un riferimento all'Arduino che sarà associato alla riga costruita;
  • riga 6: la riga corrente viene costruita dalla vista [listarduinos_item.xml];
  • righe 8–9: vengono recuperati i riferimenti ai due [TextView];
  • righe 11-12: ai due [TextView] vengono assegnati i loro valori;
  • riga 14: viene recuperato un riferimento alla casella di controllo;
  • riga 15: viene reso visibile o meno, a seconda del valore [selectable] inizialmente passato al costruttore;
  • riga 16: se la casella di controllo è presente;
  • riga 18: gli viene assegnato il valore [isChecked] dell'Arduino corrente;
  • righe 20–26: gestiamo il clic sulla casella di controllo;
  • riga 23: il valore della casella di controllo viene memorizzato nell'Arduino corrente;

Gestione dell'elenco degli Arduino

La visualizzazione dell'elenco degli Arduino è attualmente gestita da due metodi della classe [ConfigFragment]:

  • [clearArduinos]: che visualizza un elenco vuoto;
  • [showArduinos]: che visualizza l'elenco restituito dal server;

Questi due metodi funzionano come segue:


  // 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);
}

Compito: Apporta queste modifiche e prova la nuova app.


Image

5.6.10.5. La sessione

La sessione è il luogo in cui memorizziamo le informazioni condivise tra i frammenti e l'attività. Tutti i frammenti devono visualizzare l'elenco degli Arduino collegati. Quindi, una versione iniziale della sessione sarebbe simile a questa:


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
...
}

Compito: Creare la classe [Session] mostrata sopra.


Per creare questa sessione è necessario modificare il codice esistente come segue:


  // 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();
}
  • riga 18: l'elenco degli Arduino creato dalle righe precedenti viene inserito nella sessione;

5.6.10.6. Gestione dello stato del frammento

Quando il dispositivo viene ruotato, i componenti visivi della vista vengono renderizzati (per impostazione predefinita) nello stato in cui si trovavano al momento della progettazione della vista:

  • la [ListView] contiene gli elementi che il progettista vi ha inserito;
  • il messaggio di errore si trova nello stato visibile o non visibile in cui il progettista lo ha inserito;

Gli stati dei componenti visivi in fase di progettazione potrebbero essere appropriati o meno al momento del ripristino di un frammento. Qual è il caso in questione?

  • il [ListView] deve visualizzare l'elenco degli Arduino collegati. Il valore del [ListView] in fase di progettazione non può quindi essere utilizzato;
  • Il [TextView] per il messaggio di errore deve essere ripristinato allo stato visibile o nascosto che aveva al momento del salvataggio. Il suo valore in fase di progettazione potrebbe non essere adatto a questi due casi;

Dobbiamo quindi salvare lo stato di questi due componenti quando salviamo lo stato del frammento:

  • l'elenco degli Arduino collegati;
  • la visibilità (mostrato/nascosto) del messaggio di errore quando si inserisce l'URL del servizio web/JSON;

Poiché l'elenco degli Arduino è presente nella sessione, verrà salvato automaticamente. La visibilità del messaggio di errore verrà memorizzata nella seguente 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
...
}

Compito: Creare la classe [ConfigFragmentState] mostrata sopra.


Per ripristinare correttamente gli stati dei frammenti, è necessario modificare i loro metodi [getNumView] e [saveFragment]. Ad esempio, quello per il frammento [BlinkFragment] è attualmente il seguente:


  @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 non viene eseguita alcuna operazione, lo stato renderizzato alla riga 6 verrà salvato nell'elemento 0 (riga 13) dell'array CoreState[] coreStates della classe [AbstractSession] (riga 5 di seguito):


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

Tuttavia, deve essere salvato nell'elemento corrispondente all'ID del frammento [BlinkFragment] nell'array dei frammenti definito nella classe [MainActivity] (riga 9 qui sotto):


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

Gli ID dei frammenti sono stati definiti nell'interfaccia [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;
}

Alla fine, lo stato del frammento [BlinkFragment] verrà gestito correttamente se scriviamo:


  @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;
}
  • Riga 14: Restituisce l'ID del frammento [BlinkFragment] nell'array dei frammenti gestiti dall'attività;

Inoltre, la classe [CoreState], che è la classe padre degli stati dei frammenti, è attualmente la seguente (vedere la sezione 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
....
}
  • Righe 12–16: La classe [DummyFragmentState] non è elencata tra le classi figlie della classe [CoreState]. Tuttavia, il metodo [saveFragment] della classe [BlinkFragment] attualmente restituisce un tipo [DummyFragmentState]. Se lasciato così com'è, la serializzazione/deserializzazione della sessione fallirà e la sessione non verrà ripristinata, causando un crash dell'applicazione;

Il metodo [saveFragment] del frammento [BlinkFragment] deve essere riscritto come segue:


  @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]
}

Compito: in ogni frammento, modifica il metodo [getNumView] in modo che restituisca il numero del frammento e il metodo [saveFragment] in modo che restituisca un'istanza della classe di stato del frammento (come mostrato sopra).


5.6.10.7. Gestione del ciclo di vita dei frammenti

Qui ci concentriamo sul ciclo di vita del frammento [ConfigFragment], in particolare sui quattro metodi:

  • [saveFragment]: deve salvare lo stato del frammento in modo che possa essere ripristinato in seguito;
  • [initFragment]: che deve inizializzare determinati campi del frammento, se necessario. Questo metodo viene chiamato all'avvio dell'applicazione e ogni volta che il dispositivo viene ruotato. Più precisamente, viene chiamato quando il frammento diventa visibile dopo uno dei due eventi precedenti;
  • [initView]: che deve inizializzare determinati componenti della vista, se necessario. Questo metodo viene chiamato ogni volta che è stato chiamato [initFragment] e quando la vista deve essere ridisegnata perché il frammento, a un certo punto, si è spostato fuori dalla vicinanza del frammento visualizzato. Come prima, viene chiamato quando il frammento diventa visibile dopo uno di questi eventi;
  • [updateOnRestore]: che viene eseguito dopo i due metodi precedenti quando il dispositivo è stato ruotato, ma anche quando si è verificata una navigazione. Il suo ruolo è quello di ripristinare lo stato precedente del frammento;

Questi metodi saranno i seguenti:


// 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();
}
  • riga 2: l'adattatore ListView per gli Arduino. Si tratta di una variabile globale poiché viene utilizzata in diversi metodi;
  • righe 7–12: il metodo [saveFragment] salva la visibilità del TextView txtMsgErreurUrlServiceRestVisible (riga 10) in un tipo [ConfigFragmentState];
  • righe 14–19: il metodo [initFragment] inizializza l'adattatore della riga 2 con l'elenco degli Arduino attualmente presenti nella sessione (riga 17). Si noti che il ruolo di [initFragment] è quello di inizializzare i campi del frammento. In questo caso, l'inizializzazione deve essere eseguita in tutti i casi, sia che si tratti della prima visita (previousState == null) sia che non lo sia;
  • riga 17: vediamo che l'adattatore è associato alla fonte dati [session.getCheckedArduinos]. Questa non deve avere il valore null. Per questo motivo, il campo [session.checkedArduinos] viene inizializzato con un elenco vuoto nella sessione:

  // la liste des Arduinos
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
  • righe 21–35: il metodo [initView] è responsabile dell'inizializzazione di alcuni componenti dell'interfaccia visiva, in particolare quelli i cui valori non vengono conservati quando il dispositivo viene ruotato;
  • riga 24: la ListView di Arduino è associata all'adattatore della riga 2;
  • righe 28–32: la prima visita si distingue dalle altre;
  • riga 29: alla prima visita, deve essere visualizzata una [ListView] vuota. Questo perché, alla prima visita, l'adattatore [ListView] era associato a un elenco vuoto (riga 17);
  • riga 31: il messaggio di errore viene nascosto;
  • righe 32–36: il caso in cui questa non sia la prima visita;
  • il [ListView] è già nello stato corretto dalla riga 24. Non c'è altro da fare;
  • righe 34–35: il messaggio di errore viene riportato allo stato in cui si trovava quando il frammento è stato salvato l'ultima volta;
  • righe 31–36: il metodo [updateOnRestore] deve ripristinare il frammento al suo stato iniziale. Si raggiunge il metodo [updateOnRestore] in due modi:
    • o perché il dispositivo è stato ruotato. In questo caso, tutte le inizializzazioni necessarie sono già state eseguite in [initView];
    • oppure perché stiamo navigando da una scheda alla scheda [Config]. Se il frammento [Config] ha lasciato la zona dei frammenti visualizzati da quando l'abbiamo lasciato, il metodo [initView] è già stato eseguito e il frammento è già nello stato desiderato. Se il frammento [Config] non ha lasciato l'elenco dei frammenti visualizzati da quando è stato chiuso, i suoi componenti visivi non hanno cambiato stato e non c'è nulla da fare;

Vediamo che il metodo [updateOnRestore] non ha nulla da fare. A volte è così, a volte no. La differenza deriva dal metodo [updateOnSubmit]: se questo metodo esegue un'operazione che rende superflue alcune inizializzazioni effettuate in [initView], allora tali inizializzazioni dovrebbero essere eseguite nel metodo [updateOnRestore]. Prendiamo l'esempio di un pulsante di opzione con tre valori: V1, V2 e V3. Forse nel caso di una navigazione associata a un'azione [SUBMIT], il pulsante di opzione selezionato deve essere sempre quello con valore V1. In questo caso, ripristinare il valore del pulsante di opzione nel metodo [initView] è superfluo, poiché in caso di [SUBMIT] tale valore verrà sostituito da quello fornito dal metodo [updateOnSubmit]. È quindi preferibile spostare questo ripristino nel metodo [updateOnRestore] per evitare di eseguire un'operazione superflua.

  • righe 48–52: il metodo [notifyEndOfUpdates] viene eseguito dopo tutti quelli precedenti;
  • Riga 51: i pulsanti vengono riportati al loro stato iniziale: il pulsante [Refresh] viene visualizzato e il pulsante [Cancel] viene nascosto:

Compito: aggiungi il codice sopra riportato a [ConfigFragment] e poi esegui l'app. Nota che quando ruoti il dispositivo, la scheda [Config] mantiene il suo stato (messaggio di errore, elenco di Arduino). Verifica che si verifichi lo stesso comportamento quando navighi semplicemente dalla scheda [Config] alla scheda [Commands] --> scheda [Config]. In quest'ultimo caso, se hai impostato l'adiacenza del frammento su 1 in [IMainActivity], la vista [ConfigFragment] viene distrutta quando si passa alla scheda [Commands] e ricreata quando si ritorna alla scheda [Config]. Durante il test, esamina i log.


5.6.10.8. Miglioramento del codice

Il codice per il frammento [ConfigFragment] può essere migliorato. Ad esempio, abbiamo scritto:


// 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);
  }
  • Possiamo notare che nelle righe 9 e 16 stiamo utilizzando una variabile locale che non è collegata al campo della riga 2, anche se stiamo cercando di manipolare la stessa entità;

Aggiorniamo il codice come segue:


  // 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 viene eseguito il metodo alla riga 5, il ciclo di vita del frammento è stato completato. Pertanto:
    • l'adattatore alla riga 2 è stato associato alla sua fonte dati (riga 41);
    • il [ListView] degli Arduino collegati è stato collegato a questo adattatore (riga 48);

Quando vogliamo modificare la visualizzazione della [ListView], dobbiamo fare due cose:

  • modificare il contenuto della fonte dati [session.checkedArduinos];
  • notificare all'adattatore questa modifica utilizzando l'istruzione [adapterListArduinos.notifyDataSetChanged()];

È importante modificare il contenuto della fonte dati, non la fonte dati stessa. Se modifichiamo la fonte dati stessa, l'operazione [adapterListArduinos.notifyDataSetChanged()] continuerà a visualizzare la vecchia fonte dati. Dovremmo quindi associare l'adattatore alla nuova fonte dati.

Il codice è il seguente:

  • riga 27: recuperiamo l'origine dati;
  • riga 28: la svuotiamo. Per questo motivo, abbiamo rimosso il metodo [clearArduinos];
  • righe 29–31: aggiungiamo nuovi elementi a questo elenco ora vuoto;
  • riga 33: diciamo all'adattatore di aggiornare. Questo aggiornerà la visualizzazione della [ListView] associata;

Compito: apportare queste modifiche e verificare che l'applicazione funzioni ancora.


5.6.11. Comunicazione tra le viste

Per verificare la comunicazione tra le viste, faremo in modo che tutte le altre viste visualizzino l'elenco degli Arduino ottenuto dalla vista [Config]. Iniziamo con la vista [blink.xml]. Mentre in precedenza non visualizzava nulla, ora visualizzerà l'elenco degli Arduino collegati:

Image

 

Il codice XML per la vista [blink.xml] sarà il seguente:


<?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>

Questo codice è stato preso direttamente dalla vista [config.xml]. Abbiamo semplicemente modificato il margine superiore alla riga 19.


Compito: duplicare questo codice nelle viste [commands.xml, pinread.xml, pinwrite.xml].


Anche il codice del frammento [BlinkFragment] associato alla vista [blink.xml] sta cambiando:

  

  // 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);
  }
...
  • righe 2-3: il componente [ListView] per gli Arduino collegati;
  • riga 6: l'adattatore per questo [ListView];
  • righe 12-23: il codice per i metodi [initFragment] e [initView] è lo stesso già utilizzato per il frammento [ConfigFragment];
  • riga 15: quando il frammento deve essere reimpostato, reimpostiamo l'adattatore della riga 2 associandolo all'elenco degli Arduino memorizzati nella sessione. L'ultimo parametro [true] del costruttore [ListArduinosAdapter] indica che vogliamo visualizzare una casella di controllo accanto a ciascun Arduino;
  • riga 22: quando la vista del frammento deve essere reimpostata, associamo la [ListView] degli Arduino collegati all'adattatore della riga 6;

Compito: Duplica questo codice negli altri frammenti [CommandsFragment, PinReadFragment, PinWriteFragment]. Esegui l'applicazione e verifica che ogni scheda ora visualizzi l'elenco degli Arduino collegati. Verifica inoltre che, se selezioni degli Arduino in una scheda e passi a un'altra scheda, rimangano selezionati in quest'ultima.


Nota: il motivo per cui gli Arduino rimangono selezionati è il seguente. La classe [ListArduinosAdapter] è stata introdotta nella Sezione 5.6.10.4. Il codice relativo alla casella di controllo è il seguente:


        // 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);
                }
            });
}
  • Righe 11–15: Se una casella di controllo è selezionata nella scheda X, la proprietà [checked] dell'Arduino nella riga 2 viene impostata su true (riga 14);
  • quando si passa alla scheda Y, viene visualizzata la [ListView] degli Arduino presenti in quella scheda. Alla riga 9, si vede che se l'Arduino nella riga 2 ha la proprietà [checked] impostata su true, allora la casella di controllo [ck] nella riga 5 verrà selezionata;

5.6.12. Il livello [DAO]

Nota: per questa sezione, rivedere l'implementazione del livello [DAO] nel progetto [example-16B] (vedere la sezione 2.8.3).

Finora abbiamo generato manualmente l'elenco degli Arduino collegati. Ora lo richiederemo al server web / jSON. Per farlo, realizzeremo il livello [DAO]:

  

5.6.12.1. L'interfaccia IDao

L'interfaccia [IDao] del livello [DAO] sarà la seguente:


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();
}
  • righe 11-26: queste righe sono già presenti nell'interfaccia [IDao] del progetto modello [client-android-skel];
  • riga 30: il metodo [getArduinos] restituisce l'elenco degli Arduino collegati come un osservabile di tipo Observable<[Response<List<Arduino>>>] ;

Si noti che [Response<T>] è il tipo di tutte le risposte inviate dal server sotto forma di stringa 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. L'interfaccia [WebClient]

  

L'interfaccia [WebClient] è un'interfaccia per la quale la libreria AA fornisce un'implementazione. Tale interfaccia sarà la seguente:


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();
}
  • righe 15-19: queste righe sono incluse di default nell'interfaccia [WebClient] del progetto modello [client-android-skel];
  • riga 23: l'URL del server utilizzato per recuperare l'elenco degli Arduino tramite una richiesta GET. Si noti che questo URL è relativo all'URL radice [RestClientRootUrl] alla riga 16;
  • riga 24: il server restituisce una stringa JSON di tipo [Response<List<Arduino>>]. Questa stringa JSON viene automaticamente deserializzata nel tipo [Response<List<Arduino>>] utilizzando il convertitore JSON [MappingJackson2HttpMessageConverter] della riga 15;

5.6.12.3. La classe [Dao]

La classe [Dao] implementa l'interfaccia [IDao] come segue:


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();
      }
    });
  }
}
  • righe 19–87: queste righe fanno parte della classe [Dao] nel progetto [client-android-skel];
  • righe 91–100: implementazione del metodo [getArduinos];
  • riga 94: viene chiamato il metodo [getResponse] della classe padre. L'unico parametro di questo metodo è un'istanza dell'interfaccia [IRequest<T>];
  • righe 95-99: l'unico metodo dell'interfaccia [IRequest<T>] è il metodo [T getResponse()];
  • riga 94: il tipo T di [IRequest<T>] deve essere il tipo T del risultato Observable<T> del metodo alla riga 92, quindi qui, un tipo [Response<List<Arduino>>];
  • riga 97: il metodo [IRequest.getResponse()] delega il lavoro al metodo [webClient.getArduinos()] che abbiamo introdotto. [webClient], definito alla riga 24, viene istanziato dalla libreria AA ed è un'istanza dell'interfaccia [WebClient] che abbiamo introdotto;

5.6.13. La [MainActivity]

  

Abbiamo già presentato l'attività [MainActivity] nella Sezione 5.6.8. Essa estende la classe [AbstractActivity] e, in quanto tale, implementa l'interfaccia [IMainActivity], che a sua volta estende l'interfaccia [IDao]. Ogni volta che viene aggiunto un metodo all'interfaccia [IDao], esso deve essere implementato nella classe [MainActivity]. Il metodo [IDao.getArduinos] aggiunto all'interfaccia [IDao] verrà implementato come segue in [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();
  }
}
  • righe 15–18: il metodo [getArduinos] viene implementato delegando il lavoro alla classe [Dao] che abbiamo appena introdotto e alla quale abbiamo un riferimento alla riga 8;

5.6.14. Il frammento [ConfigFragment] rivisitato

Nella classe [ConfigFragment], il codice eseguito quando si fa clic sul pulsante [Refresh] è attualmente il seguente:


  @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) {
    ...
}

Dobbiamo riscrivere le righe 10–16, che codificavano in modo rigido una risposta di tipo [Response<List<Arduino>>]. Ora dobbiamo richiedere questo elenco dal livello [DAO] tramite l'attività. Il codice diventa il seguente:


  @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);
      }
    });
}
  • riga 8: l'URL radice del servizio web / JSON inserito dall'utente viene passato al livello [DAO] tramite l'attività. Questo sarà l'URL radice dell'interfaccia [WebClient] (vedere la sezione 5.6.12.2);
  • riga 10: la classe padre viene informata che sta per essere avviata un'attività asincrona;
  • righe 12–19: avvio dell'attività asincrona che restituirà l'elenco degli Arduino collegati al server;
  • riga 12: chiama il metodo [executeInBackground] della classe padre. Questo metodo richiede due parametri:
    • riga 12: il processo da osservare. Questo processo è fornito qui dal metodo [mainActivity.getArduinos()];
    • righe 12–19: un'istanza dell'interfaccia [Action1<T>], dove il tipo T è il tipo fornito dal processo, qui un tipo [Response<List<Arduino>>];
  • righe 14–18: il metodo chiamato quando l'attività asincrona restituisce il suo risultato di tipo [Response<List<Arduino>>];
  • riga 17: la risposta ricevuta viene passata al metodo [consumeArduinosResponse] scritto in precedenza;

Compito: Avvia il server come descritto nella Sezione 5.4. Collega uno o più Arduino al PC su cui è in esecuzione il server. Quindi avvia il client Android e verifica di poter recuperare correttamente l'elenco degli Arduino collegati. Osserva i log.


Image

  • Inserisci l'URL indicato al punto [1]. Si tratta di uno degli indirizzi IP del tuo server;
  • Fai clic sul pulsante [2];
  • Dovresti vedere l'elenco degli Arduino collegati in [3];

Verifica che questo elenco compaia anche nelle altre schede.

5.7. Prossimi passi


Seguendo la stessa procedura utilizzata per la vista [Config], implementa e poi testa le altre quattro viste dell'applicazione una alla volta: [Blink], [PinRead], [PinWrite] e [Commands].


Le viste da creare sono state illustrate nella Sezione 5.5.

Per ogni vista, è necessario:

  • disegnare la vista XML (vedere la Sezione 5.6.9);
  • costruire il frammento associato (vedere la Sezione 5.6.10);
  • aggiungere un metodo all'interfaccia [WebClient] (vedere la Sezione 5.6.12.2);
  • aggiungere un metodo all'interfaccia [IDao] (vedere la Sezione 5.6.12.2);
  • aggiungere un metodo alla classe [Dao] (vedere la sezione 5.6.12.3);
  • aggiungere un metodo all'attività [MainActivity] (vedere la sezione 5.6.13);
  • scrivere i gestori di eventi del frammento (vedere la sezione 5.6.14);
  • testare e osservare i log;

Nota 1: L'esempio da seguire è il progetto [Example-16B] del corso (vedi sezione 2.8.3).

Nota 2: Gli URL da interrogare e il tipo delle loro risposte sono stati presentati nella sezione 5.4.2.

Nota 3:

La classe [CommandsFragment] invia un elenco contenente un singolo comando da eseguire da uno o più Arduino. Questo comando sarà incapsulato nella seguente 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
...
}

Nell'interfaccia [WebClient], il metodo per eseguire questo elenco di comandi sarà il seguente:


  // envoi de commandes JSON
  @Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
  • riga 2: l'URL viene richiesto con una richiesta HTTP POST;
  • riga 3: il valore inviato deve avere l'annotazione [@Body];

Nota 4: Si consiglia di affrontare questo compito come segue:

  • passare alla vista successiva solo dopo che la vista corrente è stata creata e testata;
  • gestire lo stato delle viste solo dopo aver ottenuto un'applicazione funzionante in condizioni normali. Quindi, per ogni vista, scorrere il dispositivo per i diversi stati della vista e annotare eventuali informazioni perse. Questi sono i dati che devono essere salvati e poi ripristinati. Successivamente, verificare la navigazione: quando si esce da una scheda e vi si ritorna in seguito, dovrebbe trovarsi nello stesso stato in cui era quando l'ha lasciata;