Skip to content

5. TP 2 - Controllare gli Arduino con un tablet Android

Ora impareremo a controllare una scheda Arduino con un tablet. L’esempio da seguire è quello del progetto [client-android-skel] del corso (cfr. paragrafo 2).

5.1. Architettura del progetto

L’intero progetto avrà la seguente architettura:

  • il blocco [1], il server web / jSON e gli Arduino vi saranno forniti;
  • dovrete realizzare il blocco [2] e occuparvi della programmazione del tablet Android per interagire con il server web / jSON.

5.2. L'attrezzatura

Avete a disposizione i seguenti componenti:

  • un Arduino con espansione 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 procedere per collegare tra loro i vari componenti:

  • rimuovere il cavo di rete dal PC;
  • collegare il PC e l'Arduino tramite un cavo di rete;
  • l'Arduino a vostra disposizione sarà già stato programmato. Il suo indirizzo IP sarà [192.168.2.2]. Affinché il vostro PC riconosca l’Arduino, è necessario assegnargli l’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:

Accedere a [Panneau de configuration\Réseau et Internet\Centre Réseau et partage]:

 
  • in [1], clicca sul link [réseau local];
  • in [2], clicca sul pulsante [Propriétés] della rete locale;
  • in [3], cliccare sulle proprietà [IPv4] della scheda [réseau local];
  • in [4], assegnare a questa scheda l'indirizzo IP [192.168.2.1] e la subnet mask [255.255.255.0];
  • in [5], cliccare su [OK] tutte le volte necessarie per uscire dalla procedura guidata.

5.2.2. Il tablet

  • utilizzando la vostra chiave Wi-Fi, collegate il vostro computer alla rete Wi-Fi che vi verrà indicata. Fate lo stesso con il vostro tablet;
  • Verificate l'indirizzo Wi-Fi del vostro IP digitando [ipconfig] in una finestra DOS. Troverete un indirizzo del tipo [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
  • verificate l'indirizzo Wi-Fi IP del vostro tablet. Se non sapete come fare, chiedete al vostro supervisore. Troverete un indirizzo del tipo [192.168.x.z];
  • disattivate il firewall del vostro PC se è attivo [Panneau de configuration\Système et sécurité\Pare-feu Windows];
  • in una finestra DOS, verificate 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 vostro 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 sistema è ora pronta.

5.2.3. L'emulatore [Genymotion]

L'emulatore [Genymotion] (vedi paragrafo 6.9) sostituisce vantaggiosamente il tablet. È quasi altrettanto veloce e non richiede una rete Wi-Fi. Si consiglia di utilizzare questo metodo. Potrete utilizzare il tablet per la verifica finale della vostra applicazione.

5.3. Programmazione degli Arduino

Qui ci occupiamo della scrittura del codice C per gli Arduino:

Da leggere

  • installazione dell’ambiente di sviluppo Arduino (cfr. paragrafo 6.1);
  • utilizzo delle librerie jSON (Appendici, paragrafo 6.6);
  • nell'ambiente di sviluppo Arduino, testare l'esempio di un server (ad esempio il server web) e quello di un client (ad esempio il client Telnet);
  • gli allegati sull’ambiente di programmazione degli Arduino al paragrafo 6.1.

Un Arduino è un insieme di pin collegati all'hardware. Questi pin sono ingressi o uscite. Il loro valore è binario o analogico. Per comandare l'Arduino, ci saranno due operazioni di base:

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

A queste due operazioni di base ne aggiungeremo una terza:

  • far lampeggiare un LED per un certo periodo di tempo e con una certa frequenza. Questa operazione può essere eseguita richiamando ripetutamente le due operazioni di base precedenti. Tuttavia, vedremo durante i test che gli scambi tra il modulo [DAO] e un Arduino avvengono nell’ordine di un secondo. Non è quindi possibile far lampeggiare un LED ogni 100 millisecondi, ad esempio. Implementeremo quindi questa funzione di lampeggiamento direttamente sull’Arduino stesso.

Il funzionamento dell’Arduino sarà il seguente:

  • le comunicazioni tra il livello [DAO] e un Arduino avvengono tramite una rete TCP-IP mediante 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 presente nel livello [DAO]. Invia al server un'unica riga di testo:
{"id":"cuisine","desc":"duemilanove","mac":"90:A2:DA:00:1D:A7","port":102}

Si tratta di una stringa jSON che identifica l’Arduino che si connette:

  • id: un identificativo dell'Arduino;
  • desc: una descrizione delle funzionalità dell’Arduino. In questo caso è stato semplicemente indicato il tipo di Arduino;
  • mac: indirizzo MAC dell’Arduino;
  • port: il numero della porta su cui l’Arduino attenderà i comandi dal livello [DAO].

Tutte queste informazioni sono stringhe di caratteri, tranne la porta, che è un numero intero.

  • una volta che l’Arduino si è registrato presso il server di registrazione, si mette in ascolto sulla porta che ha indicato al server (102 sopra). Attende comandi jSON della seguente forma:
{"id":"identifiant","ac":"une_action","pa":{"param1":"valeur1","param2":"valeur2",...}}

Si tratta di una stringa jSON con i seguenti elementi:

  • id: un identificativo del comando. Può essere qualsiasi valore;
  • ac: un'azione. Ce ne sono tre:
  • pw (pin write) per scrivere un valore su un pin,
  • pr (pin read) per leggere il valore di un pin,
  • cl (lampeggiare) per far lampeggiare un LED;
  • pa: i parametri dell'azione. Dipendono dall'azione.
  • Arduino invia sistematicamente una risposta al proprio client. Si tratta di una stringa jSON della seguente forma:
{"id":"1","er":"0","et":{"pinx":"valx"}}

dove

  • id: l'identificativo del comando a cui si risponde;
  • er (errore): un codice di errore se si è verificato un errore, 0 in caso contrario;
  • e (stato): un dizionario sempre vuoto tranne che per il comando di lettura pr. In tal caso, il dizionario contiene il valore del pin n. x richiesto.

Ecco alcuni esempi volti a chiarire le specifiche precedenti:

Far 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 in millisecondi di un lampeggio, il numero nb di lampeggi, il numero del pin dell'LED.

Scrivere il valore binario 1 sul pin n. 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à mod b (binaria) o a (analogica) della scrittura, il valore val da scrivere, il n. del pin. Per una scrittura binaria, val è 0 o 1. Per una scrittura analogica, val rientra nell'intervallo [0,255].

Scrivere il valore analogico 120 sul pin n. 2:

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

Leggere 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à mod b (binaria) o a (analogica) della lettura, il numero del pin. Se non ci sono errori, Arduino inserisce nel dizionario "et" della sua risposta il valore del pin richiesto. In questo caso, pin0 indica che è stato richiesto il valore del pin n. 0 e 1023 è tale valore. In lettura, un valore analogico rientrerà nell’intervallo [0, 1024].

Abbiamo presentato i tre comandi cl, pw e pr. Ci si potrebbe chiedere perché non siano stati utilizzati campi più espliciti nelle stringhe jSON, come "action" al posto di "ac", "pinwrite" al posto di "pw", "parametri" al posto di "pa", ... Un Arduino ha una memoria molto limitata. Tuttavia, le stringhe jSON scambiate con l'Arduino incidono sull'occupazione della memoria. Abbiamo quindi scelto di accorciarle il più possibile.

Vediamo ora alcuni casi di errore:

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

È stato inviato un comando che non è nel formato jSON. L'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 omettendo 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":{}}

È stato inviato un comando pinread sconosciuto (è pr). L'Arduino ha restituito il codice di errore 104.

Non proseguiremo con gli esempi. La regola è semplice. L'Arduino non deve bloccarsi, indipendentemente dal comando che gli viene inviato. Prima di eseguire un comando jSON, si assicura che sia corretto. Non appena si verifica un errore, l’Arduino interrompe l’esecuzione del comando e restituisce al cliente la stringa di errore jSON. Anche in questo caso, a causa dei limiti di spazio in memoria, viene restituito un codice di errore anziché un messaggio completo.

Il codice del programma eseguito su Arduino è riportato negli esempi di questo documento:

  

Per trasferirlo su Arduino:

  • collegatelo al vostro PC;
  • su [1], aprite il file [arduino_uno.ino]. L'Arduino IDE si avvierà e caricherà il file;

Nota: il codice è stato originariamente creato e testato con una versione 1.5.x di IDE ARDUINO. Da allora sono state rilasciate altre versioni di IDE. Il codice non ha funzionato con una versione 1.6.x di IDE ARDUINO. 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 su quale porta seriale del PC si trova;
  • nel [8], caricare il programma [arduino_uno] sull'Arduino;

Il codice del programma è ampiamente commentato. Il lettore interessato potrà consultarlo. Segnaliamo semplicemente le righe di codice che consentono di configurare 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
// indirizzo MAC dell'Arduino UNO
byte macArduino[] = { 
  0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// l'indirizzo IP dell'Arduino
IPAddress ipArduino(192,168,2,2);
// il suo identificativo
char * idArduino="cuisine";
// porta del server Arduino
int portArduino=102;
// descrizione dell'Arduino
char * descriptionArduino="contrôle domotique";
// il server Arduino funzionerà sulla porta 102
EthernetServer server(portArduino);
// IP del server di registrazione
IPAddress ipServeurEnregistrement(192,168,2,1); 
// porta del server di registrazione
int portServeurEnregistrement=100;
// il client Arduino del server di registrazione
EthernetClient clientArduino;
// il comando del client
char commande[100];
// risposta di Arduino
char message[100];

// inizializzazione
void setup() {
  // Il monitor seriale consentirà di seguire gli scambi
  Serial.begin(9600);
  // avvio della connessione Ethernet
  Ethernet.begin(macArduino,ipArduino);  
  // memoria disponibile
  Serial.print(F("Memoire disponible : "));
  Serial.println(freeRam());
}

// ciclo infinito
void loop()
{
  ...
}
  • riga 8: l'indirizzo MAC dell'Arduino. In questo contesto non ha molta importanza, poiché l'Arduino si troverà su una rete privata in cui sono presenti un PC e uno o più Arduino. È sufficiente che l’indirizzo MAC sia univoco su questa rete privata. Normalmente, la scheda di rete dell’Arduino presenta un adesivo su cui è indicato l’indirizzo MAC della scheda. Se tale adesivo è assente e non si conosce l’indirizzo MAC della scheda, è possibile inserire qualsiasi valore nella riga 8, purché venga rispettata la regola di unicità dell’indirizzo MAC sulla rete privata;
  • riga 11: l'indirizzo IP della scheda. Anche in questo caso, si inserisce un valore a scelta del tipo [192.168.2.x], variando x a seconda dei diversi Arduino presenti nella rete privata;
  • riga 13: identificativo dell’Arduino. Deve essere unico tra gli identificativi degli Arduino della stessa rete privata;
  • riga 15: la porta di servizio dell’Arduino. Si può inserire qualsiasi valore;
  • riga 17: la descrizione della funzione dell’Arduino. È possibile inserire qualsiasi valore. Attenzione alle stringhe lunghe a causa della memoria limitata dell’Arduino;
  • riga 21: indirizzo IP del server di registrazione dell'Arduino su PC. Non deve essere modificato;
  • riga 23: porta di questo servizio di registrazione. Non deve essere modificata;

5.4. Il server web / jSON

5.4.1. Installazione

Image

Il file binario Java del server web / jSON è il seguente:

 

Apri una finestra di comando e digita il seguente comando:

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

Se [java.exe] non è presente nel PATH della finestra dei comandi, sarà necessario digitare il percorso completo di [java.exe] (in genere C:\Program Files\java\...).

Si aprirà una finestra DOS che mostrerà 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 [/**] verso il gestore di tipo [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 di tipo [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 integrato;
  • riga 15: la servlet [dispatcherServlet] di Spring MVC viene caricata ed eseguita;
  • riga 18: viene rilevata la Rest URL [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}];
  • riga 19: viene rilevata la Rest URL [/arduinos/commands/{idArduino}];
  • riga 20: viene rilevato l'URL Rest [/arduinos/];
  • riga 21: viene rilevato l'URL Rest [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}];
  • riga 22: viene rilevato l'URL Rest [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}];
  • riga 26: viene avviato il server di registrazione degli Arduino;

Collegate il vostro Arduino al PC, se non l'avete già fatto. Il firewall del PC deve essere disattivato. Quindi, tramite un browser, accedete al URL [http://localhost:8080/arduinos]:

Dovrebbe comparire l'ID dell'Arduino collegato. Se non viene visualizzato nulla, provate a resettare l'Arduino. È dotato di un pulsante a pressione apposito.

Il server web / jSON è ora installato.

5.4.2. I URL esposti dal servizio web / jSON

Da leggere: progetto [Exemple-15] (cfr. paragrafo 1.16.1);

Il servizio web / jSON è stato implementato con Spring MVC ed espone le seguenti URL:


@Controller
public class WebController {

  // livello applicativo
  @Autowired
  private IMetier métier;

  // elenco degli Arduino
  @RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String getArduinos() throws JsonProcessingException {
    ...
  }

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

  // invio di comandi 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 {
    ...
  }

  // lettura pin
  @RequestMapping(value = "/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String pinRead(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode) throws JsonProcessingException {
    ....
  }

  // scrittura su 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> {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // eventuali messaggi di stato
    private List<String> messages;
    // corpo della risposta
    private T body;

    // costruttori
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getter e setter
...
}

Il URL [/arduinos] invia 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 {
  // dati
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;

// getter e setter
...
}
  • riga 7: [id] è l'identificativo dell'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 attesa di comandi;

I URL:

  • [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}];
  • [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}];
  • [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}];
  • [/arduinos/commands/{idArduino}];

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


public class ArduinoResponse implements Serializable {
  
  private String json;
  private String id;
  private String erreur;
  private Map<String, Object> etat;

  // getter e setter
...
}
  • [json]: la stringa jSON inviata da un Arduino e che non è stato possibile decodificare (caso di errore), altrimenti null;
  • [id]: l'identificativo del comando a cui risponde l'Arduino;
  • [erreur]: un codice di errore, 0 se OK, altro valore in caso contrario;
  • [etat]: un dizionario contenente la risposta specifica al comando. È solitamente vuoto, a meno che il comando non richiedesse la lettura di un valore dall’Arduino, nel qual caso tale valore verrà inserito in questo dizionario;

5.4.3. Test del servizio web / jSON

Prendete confidenza con il server web / jSON testando i seguenti URL:

URL
rôle
http://localhost:8080/arduinos/
rend la liste des Arduinos connectés
http://localhost:8080/arduinos/
blink/1/cuisine/8/100/20/
fait clignoter la led de la pin n° 8
 de l'Arduino identifié par cuisine,
 20 fois toutes les 100 ms.
http://localhost:8080/arduinos/
pinRead/1/cuisine/0/a/
lecture analogique de la pin n° 0 de
 l'Arduino identifié par cuisine
http://localhost:8080/arduinos/
pinRead/1/cuisine/5/b/
lecture binaire de la pin n° 5 de
 l'Arduino identifié par cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/8/b/1/
écriture binaire de la valeur 1 sur la pin n° 8 de l'Arduino identifié par
 cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/4/a/100/
écriture analogique de la valeur 100 sur la pin n° 4 de l'Arduino identifié
 par cuisine

Ecco alcuni screenshot di ciò che dovreste ottenere:

Ottenere l'elenco degli Arduino collegati:

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

  • [status]: se pari a 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]: identificativo dell'Arduino. Due Arduino non possono avere lo stesso identificativo;
    • [description]: breve descrizione della funzionalità dell’Arduino;
    • [mac]: indirizzo MAC dell’Arduino;
    • [ip]: indirizzo IP dell'Arduino;
    • [port]: porta su cui è in attesa di comandi;

Far lampeggiare il LED del pin n. 8 dell'Arduino identificato da [cuisine], 20 volte ogni 100 ms:

 

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

  • [status]: se pari a 0 indica che non si è verificato alcun errore; in caso contrario, si è verificato un errore;
  • [messages]: un elenco di messaggi che spiegano l'errore, qualora si sia verificato un errore:
  • [body]: la risposta di Arduino in assenza di errori:
    • [id]: identificativo del comando. Questo identificativo è il 1 presente in [/blink/1]. L'Arduino riporta questo identificativo del comando nella sua risposta;
    • [erreur]: un numero di errore. Un valore diverso da 0 indica la presenza di un errore;
    • [etat]: viene utilizzato solo per la lettura di un pin. Il suo valore corrisponde quindi al valore del pin;
    • [json]: viene utilizzato solo in caso di errore jSON tra il client e il server. Il suo valore è quindi la stringa errata jSON inviata dall’Arduino;

Lettura analogica del pin n. 0 dell’Arduino identificato da [cuisine]:

 

La stringa jSON ricevuta dal server web / jSON è analoga alla precedente, con la sola differenza del campo [etat] che rappresenta il valore del pin n. 0.

Lettura binaria del pin n. 5 dell'Arduino identificato da [cuisine]:

 

La stringa jSON ricevuta dal server web / jSON è analoga alla precedente.

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

 

La stringa jSON ricevuta dal server web / jSON è analoga alla precedente.

Il test di URL [http://localhost:8080/arduinos/commands/cuisine] è più complesso. Il metodo del server web / jSON che elabora questa URL richiede una richiesta POST che non è possibile simulare semplicemente con un browser. Per testare questo URL, è possibile utilizzare un browser Chrome con l’estensione [Advanced REST Client] (cfr. paragrafo 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 è quello di jSON;
  • in [5], la stringa jSON inviata. Si notino bene le parentesi quadre che aprono e chiudono l'elenco. Qui, nell'elenco c'è solo un comando jSON che fa lampeggiare il pin n. 8, 10 volte ogni 100 ms;
  • in [6], si invia la richiesta;
 
  • 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 dell'Arduino a ciascuno dei comandi jSON inviati.

Vediamo cosa succede quando si invia un comando jSON sintatticamente errato per l’Arduino:

Si riceve quindi la seguente risposta:

 

Si nota che nella risposta di Arduino il numero di errore è [104], a indicare che il comando [xx] non è stato riconosciuto.

5.5. Test sul client Android

Ecco il file binario eseguibile del client Android completato:

  

Con il mouse, trascinate il file binario [app-debug.apk] sopra indicato su un emulatore di tablet [GenyMotion]. Verrà quindi salvato ed eseguito. Avviare anche il server web / jSON se non è già stato fatto. Collegare l'Arduino a PC con un LED montato su di esso. Il client Android consente di gestire gli Arduino da remoto. Presenta all'utente le seguenti schermate.

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

Image

  • in [1], inserite l'indirizzo IP [192.168.2.1] assegnato al vostro PC (cfr. paragrafo 5.2).

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

Image

Image

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

Image

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

Image

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

Image

5.6. Il client Android del servizio web / jSON

Passiamo ora alla scrittura del client Android.

5.6.1. L'architettura del client

L’architettura del client Android sarà quella del progetto [Exemple-15] (cfr. paragrafo 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, si desidera far lampeggiare due LED posizionati su due Arduino contemporaneamente e non uno dopo l’altro. Pertanto, il nostro client Android utilizzerà un’attività asincrona per ogni Arduino e tali attività verranno eseguite in parallelo.

5.6.2. Il progetto Android Studio del client

Duplicare il progetto [client-android-skel] (cfr. paragrafo 2) nel progetto [client-arduinos-01] (se necessario, consultare le istruzioni su come duplicare un progetto Gradle al paragrafo 1.15):

Image

5.6.3. Le cinque viste XML

  

Ci saranno cinque viste XML:

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

Per il momento, 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 in un contenitore [RelativeLayout] (righe 7-10), a sua volta incluso in un contenitore [ScrollView] (righe 2-11). Questo 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 di un progetto realizzato con [client-android-skel] devono essere associati a un menu, anche se vuoto. In questo caso, l’applicazione non avrà alcun menu. Il menu vuoto è già presente nel progetto;

  

5.6.5. I cinque frammenti dell’applicazione

 

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


Il frammento [ConfigFragment] presenta la seguente struttura:


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 {

  // campi ereditati dalla classe padre -------------------------------------------------------
...

Sostituite la riga 10 con la seguente:


@EFragment(R.layout.config)

Operazione: procedere allo stesso modo per gli altri quattro frammenti, adattando l’attributo [@EFragment] della classe.


Frammento
Vista
ConfigFragment

R.layout.config
PinReadFragment

R.layout.pinread
PinWriteFragment

R.layout.pinwrite
CommandsFragment

R.layout.commands
BlinkFragment

R.layout.blink

5.6.6. Stati dei frammenti

Ogni frammento avrà uno stato.


Compito: duplicare cinque volte la classe [DummyFragmentState], per creare i cinque stati presentati 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. In questo caso, l’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 {

  // accesso alla sessione
  ISession getSession();

  // cambio di vista
  void navigateToView(int position, ISession.Action action);

  // gestione dell'attesa
  void beginWaiting();

  void cancelWaiting();

  // costanti dell'applicazione -------------------------------------

  // modalità debug
  boolean IS_DEBUG_ENABLED = true;

  // tempo massimo di attesa per la risposta del server
  int TIMEOUT = 1000;

  // tempo di attesa prima dell'esecuzione della richiesta del client
  int DELAY = 000;

  // autenticazione di base
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adiacenza dei frammenti
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra delle schede
  boolean ARE_TABS_NEEDED = true;

  // immagine di attesa
  boolean IS_WAITING_ICON_NEEDED = true;

  // numero di frammenti
  int FRAGMENTS_COUNT = 5;

  // numero di visualizzazioni
  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 presenta delle schede;
  • riga 43: questa applicazione ha cinque frammenti;
  • righe 46-50: i numeri dei cinque frammenti;
  • riga 34: adiacenza dei frammenti. Lo sviluppatore può inserire 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 {
  // frammento visitato o meno
  protected boolean hasBeenVisited = false;
  // stato dell'eventuale menu del frammento
  protected MenuItemState[] menuOptionsState;

  // getter e setter
...
}
  • righe 12-16: qui è necessario dichiarare le classi degli stati dei cinque frammenti;

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 {

  // livello [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessione
  private Session session;

  // metodi della classe padre -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessione
    this.session = (Session) super.session;
    // creazione delle cinque schede
    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) {
    // visualizzazione del frammento n. posizione
    navigateToView(position, ISession.Action.NAVIGATION);
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }

  // implementazione IDao -----------------------------------------
}
  • righe 46-50: creazione delle cinque schede dell'applicazione;
  • riga 48: i titoli delle schede sono forniti dal metodo delle righe 63-79;
  • i cinque frammenti vengono istanziati alla riga 60. A causa delle annotazioni AA, le classi dei frammenti sono quelle presentate in precedenza con un trattino basso come suffisso;
  • righe 63-79: si definisce un titolo per ciascuno dei frammenti. Questi titoli saranno ricercati nel file [res / values / strings.xml]
  

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


<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- nome dell'applicazione -->
  <string name="app_name">[arduinos-client-01]</string>
  <!-- Frammenti e schede -->
  <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>

Esercizio: create gli elementi precedenti e compilate il progetto. Non dovrebbero esserci errori.


Eseguite il progetto. Sull’emulatore dovreste ottenere la seguente schermata:

Image

Esaminate i log che hanno accompagnato la visualizzazione della prima vista e seguite la traccia delle diverse fasi eseguite. Passate da una scheda all’altra e continuate a seguire i log.

5.6.9. La vista XML [config]

La vista XML [config] sarà la seguente:

La visualizzazione sopra riportata si ottiene con il 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 di caratteri (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>

    <!-- Frammenti e schede -->
    <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>

    <!-- Configurazione -->
    <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 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 dimensioni (android:textSize alla riga 16) definite nel file [res / values / dimens]:

  

<resources>
  <!-- Margini predefiniti dello schermo, secondo le linee guida di progettazione Android. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="fab_margin">16dp</dimen>
  <dimen name="appbar_padding_top">8dp</dimen>
  <!-- app -->
  <dimen name="titre">30dp</dimen>
</resources>

Questa tecnica non è stata utilizzata per tutte le dimensioni. Tuttavia, è quella consigliata. Consente di modificare le dimensioni in un unico punto.


Esercizio: create gli elementi sopra indicati.


Eseguite nuovamente il vostro progetto. Dovreste ottenere la seguente visualizzazione:

Image

5.6.10. Il frammento [ConfigFragment]

  

Per gestire la nuova vista [config], il codice del frammento [ConfigFragment] viene modificato 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 {

  // Elementi dell'interfaccia visiva
  @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() {
  }

  // gestione del ciclo di vita del frammento -------------------------------------

  @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) {
    // Prima visita?
    if(previousState==null){
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
  }

  @Override
  protected void notifyEndOfUpdates() {
    // pulsanti
    initButtons();
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
  }

  // metodi privati --------------------------------------------

  private void initButtons() {
    // il pulsante [Exécuter] sostituisce il pulsante [Annuler]
    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 verrà visualizzato, il pulsante [Annuler] verrà nascosto (riga 82) e il pulsante [Rafraîchir] verrà visualizzato (righe 86-87). Infatti, in questa applicazione, un frammento non può essere visualizzato mentre è in corso un'operazione asincrona e quindi il pulsante [Annuler] è visibile;

Compito: create gli elementi sopra indicati.


Eseguite questa nuova versione. La prima vista dovrebbe ora essere la seguente:

Image

5.6.10.1. Il pulsante [Rafraîchir]

Per il momento gestiremo il clic sul pulsante [Rafraîchir] nel modo seguente:


@Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // si avvia un'attività - si prepara l'attesa
    beginWaiting(1);
  }

  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // si annullano le attività asincrone
    cancelRunningTasks();
  }

  protected void beginWaiting(int numberOfRunningTasks) {
    // si prepara l'attesa delle attività
    beginRunningTasks(numberOfRunningTasks);
    // il pulsante [Annuler] sostituisce il pulsante [Rafraîchir]
    btnRafraichir.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
}
  // gestione del ciclo di vita del frammento -------------------------------------
...
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // pulsanti nel loro stato iniziale
    initButtons();
  }

  // metodi privati --------------------------------------------

  private void initButtons() {
    // il pulsante [Exécuter] sostituisce il pulsante [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
  • righe 1-5: il metodo eseguito quando si clicca sul pulsante [Rafraîchir];
  • riga 4: si avvia l'attesa;
  • riga 18: si passa alla classe padre il numero di attività asincrone che si intende avviare. Apparirà l’immagine di attesa;
  • righe 20-21: questa attesa comporterà la comparsa del pulsante [Annuler], la scomparsa del pulsante [Rafraîchir] e la comparsa dell'immagine di attesa. Non accade nient’altro. L’utente può tuttavia fare clic sul pulsante [Annuler]. Verrà quindi eseguito il metodo delle righe 7-14;
  • riga 13: si richiede alla classe padre di annullare tutte le attività. La classe lo farà e richiamerà a sua volta il metodo delle righe 25-29 per segnalare che tutte le attività sono terminate. Il parametro [runningTasksHaveBeenCanceled] assumerà il valore true per indicare che le attività sono state annullate;
  • righe 35-36: il pulsante [Annuler] scomparirà, mentre il pulsante [Rafraîchir] riapparirà.

Esercizio: apportate queste modifiche, quindi eseguite il progetto. Verificate che il pulsante [Rafraîchir] avvii l’attesa e che il pulsante [Annuler] la interrompa. Osservate i log.


5.6.10.2. Verifica dei dati inseriti

Nella versione precedente non verificavamo la validità dell’input URL. Per verificarla, aggiungiamo il seguente codice in [ConfigFragment]:


// i valori inseriti
  private String urlServiceRest;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // si verificano i dati inseriti
    if (!pageValid()) {
      return;
    }
    // si avvia un'attività - si prepara l'attesa
    beginWaiting(1);
  }

  // verifica dei dati inseriti
  private boolean pageValid() {
    // inizialmente nessun messaggio di errore
    txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    // si recuperano l'IP e la porta del server
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    // si verifica la validità
    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) {
      // visualizzazione del messaggio di errore
      txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
      // ritorno a UI
      return false;
    }
    // Tutto a posto
    return true;
  }
  • riga 2: il valore immesso;
  • righe 7-9: prima di fare qualsiasi cosa, si verifica la validità dei dati inseriti;
  • riga 19: si recupera l’URL inserito e vi si aggiunge il prefisso [http://];
  • riga 22: si tenta di creare un oggetto URI (Uniform Resource Identifier) con esso. Se il valore URL inserito è sintatticamente errato, si verificherà un'eccezione;
  • righe 23-27: si genera un'eccezione se l'URI è corretto ma sono presenti anche [host==null] e [port==-1]. Si tratta di un caso possibile;
  • riga 30: si è verificata un'eccezione. Viene visualizzato il messaggio di errore;
  • riga 32: si restituisce [false] per indicare che la pagina non è valida;
  • riga 35: non si sono verificati errori. Si restituisce [true] per indicare che la pagina è valida;

Compito: create gli elementi sopra indicati.


Testate questa nuova versione e verificate che i URL non validi vengano correttamente segnalati.

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 è stato aggiunto un valore booleano per indicare se l’Arduino è stato selezionato o meno in un elenco;

La classe [Arduino] è quella già utilizzata dal server e descritta nel paragrafo 5.4.2. È la seguente:


package android.arduinos.entities;

import java.io.Serializable;

public class Arduino implements Serializable {
  // dati
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;

// getter e setter
...
}
  • riga 7: [id] è l'identificativo dell'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 attesa di comandi;

Questa classe corrisponde alla stringa jSON ricevuta dal server quando gli viene richiesta la lista 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;
    // È possibile selezionare un Arduino
    private boolean isChecked;

    // costruttore
    public CheckedArduino(Arduino arduino, boolean isChecked) {
        // genitore
        super(arduino.getId(), arduino.getDescription(), arduino.getMac(), arduino.getIp(), arduino.getPort());
        // locale
        this.isChecked = isChecked;
    }

    // getter e setter
    public boolean isChecked() {
        return isChecked;
    }

    public void setChecked(boolean isChecked) {
        this.isChecked = isChecked;
    }

}
  • riga 3: la classe [CheckedArduino] eredita dalla classe [Arduino];
  • riga 6: le viene aggiunto un valore booleano che ci servirà per sapere se, nell'elenco degli Arduino visualizzato, è stato selezionato o meno un Arduino;

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() {
    // si verificano i dati inseriti
    if (!pageValid()) {
      return;
    }
    // si avvia un'attività - si prepara l'attesa
    beginWaiting(1);
    // si ripulisce l'elenco degli Arduino
    clearArduinos();
    // si richiede l'elenco degli Arduino in background
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
   ...
  }

  // si azzera l'elenco degli Arduino
  private void clearArduinos() {
    // si crea un elenco vuoto
    List<String> strings = new ArrayList<>();
    // visualizzarla
    listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
  • riga 2: il codice ListView che visualizza gli Arduino collegati al server;
  • riga 5: il metodo che richiede l'elenco degli Arduino connessi;
  • riga 11: si comunica alla classe padre che si sta per avviare un'operazione asincrona;
  • riga 12: si cancella l'elenco degli Arduino attualmente visualizzato;
  • riga 15: si richiede in background l'elenco degli Arduino connessi;
  • righe 23-28: il metodo che cancella l'elenco degli Arduino attualmente visualizzato;

Il metodo [getArduinosInBackground] è il seguente:


  private void getArduinosInBackground() {
    // si crea un elenco fittizio di Arduino
    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));
    }
    // si simula una risposta del server
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // si annulla l'attesa
    cancelWaitingTasks();
    // si modificano i pulsanti
    initButtons();
    // si elabora la risposta
    consumeArduinosResponse(response);
}
  • righe 3-6: si crea un elenco di 20 Arduino;
  • righe 8-9: si costruisce la risposta di tipo [Response<List<Arduino>>] (paragrafo 5.4.2) che incapsulerà l'elenco degli Arduino creato;
  • riga 11: si annulla l'attesa;
  • riga 13: si riportano i pulsanti al loro stato iniziale;
  • riga 15: si elabora la risposta;

Il metodo [consumeArduinosResponse] è il seguente:


  // visualizzazione della risposta
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // errore?
    if (response.getStatus() != 0) {
      // visualizzazione
      showAlert(response.getMessages());
      // ritorno all'interfaccia utente
      return;
    }
    // si crea un elenco di [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // li visualizziamo
    showArduinos(checkedArduinos);
}
  • righe 4-11: si controlla il codice di errore della risposta inviata dal server:
  • riga 4: se il codice di errore è diverso da zero;
  • riga 6: si visualizzano i messaggi memorizzati dal server nel campo [messages] della risposta;
  • riga 8: si torna all'interfaccia utente;
  • righe 11-16: se non ci sono stati errori, si 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) {
    // si crea un elenco di String a partire dall'elenco degli Arduino
    List<String> strings = new ArrayList<>();
    for (CheckedArduino checkedArduino : checkedArduinos) {
      strings.add(checkedArduino.toString());
    }
    // la si visualizza
    listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}

Compito: apportate le modifiche sopra indicate ed eseguite il vostro progetto.


Cliccando sul pulsante [Rafraîchir] dovreste ottenere la seguente schermata:

Image

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

5.6.10.4. Un modello per visualizzare un Arduino

Al momento, gli Arduino collegati vengono visualizzati nella vista [Config] nel modo seguente:

Image

Ora si desidera visualizzarli come segue:

Image

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

Quanto segue riprende i concetti sviluppati nei progetti [exemple-19] e [exemple-19B] del paragrafo 1.20. Rivedeteli se necessario.

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

 

Il codice della 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 dell'Arduino;
  • righe 35-43: il testo [Description : ];
  • righe 45-53: qui verrà inserita la descrizione dell'Arduino;

Questa vista utilizza testi (righe 23, 32, 43) definiti 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 inoltre 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 di un elemento dell'elenco degli Arduino

  

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


package istia.st.android.vues;

import istia.st.android.R;
...

public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {

    // la tabella degli Arduino
    private List<CheckedArduino> arduinos;
    // il contesto di esecuzione
    private Context context;
    // l'ID del layout di visualizzazione di una riga dell'elenco degli Arduino
    private int layoutResourceId;
    // la riga contiene o meno una casella di controllo
    private Boolean selectable;

    // costruttore
    public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
        // genitore
        super(context, layoutResourceId, arduinos);
        // si memorizzano le informazioni
        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’identificativo della vista da visualizzare per ciascun elemento della fonte dati, la fonte dati che alimenta l’elenco, 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] ha il compito di generare la vista n. [position] all’interno di [ListView] e di gestirne gli eventi. Il suo codice è il seguente:


@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // l'Arduino corrente
        final CheckedArduino arduino = arduinos.get(position);
        // si crea la riga corrente
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // si recuperano i riferimenti sui [TextView]
        TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
        TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
        // si compila la riga
        txtArduinoId.setText(arduino.getId());
        txtArduinoDesc.setText(arduino.getDescription());
        // il CheckBox non è sempre visibile
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // gli si assegna il valore
            ck.setChecked(arduino.isChecked());
            // si gestisce il clic
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
        }
        // si rende la riga
        return row;
    }
  • riga 2: il primo parametro è la posizione nel [ListView] della riga da creare. È anche la posizione nell'elenco degli Arduino memorizzato localmente;
  • riga 4: si recupera un riferimento all’Arduino che verrà associato alla riga costruita;
  • riga 6: la riga corrente viene creata a partire dalla vista [listarduinos_item.xml];
  • righe 8-9: vengono recuperati i riferimenti ai due [TextView];
  • righe 11-12: ai due [TextView] viene assegnato il loro valore;
  • riga 14: si recupera un riferimento alla casella di controllo;
  • riga 15: la casella di controllo viene resa 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: si gestisce 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 si evolvono come segue:


  // si azzera l'elenco degli Arduino
  private void clearArduinos() {
    // si visualizza un elenco vuoto
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }

  // visualizzazione dell'elenco degli Arduino
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // visualizza gli Arduino
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
}

Compito: apportare queste modifiche e testare la nuova applicazione.


Image

5.6.10.5. La sessione

La sessione è il luogo in cui inseriamo le informazioni condivise dai frammenti e dall'attività. Tutti i frammenti devono visualizzare l'elenco degli Arduino collegati. Pertanto, una prima versione della sessione sarà la seguente:


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 {
  // dati da condividere tra i frammenti stessi e tra i frammenti e l'attività
  // gli elementi che non possono essere serializzati in jSON devono avere l'annotazione @JsonIgnore
  // non dimenticare i getter e i setter necessari per la serializzazione/deserializzazione in jSON

  // l'elenco degli Arduino
  private List<CheckedArduino> checkedArduinos = new ArrayList<>();

  // getter e setter
...
}

Compito: create la classe [Session] sopra indicata.


La creazione di questa sessione ci porta a modificare il codice già scritto nel modo seguente:


  // visualizzazione risposta
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // errore?
    if (response.getStatus() != 0) {
      // visualizzazione
      showAlert(response.getMessages());
      // annullamento
      doAnnuler();
      // ritorno all'interfaccia utente
      return;
    }
    // si crea un elenco di [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // la si inserisce nella sessione
    session.setCheckedArduinos(checkedArduinos);
    // li visualizziamo
    showArduinos(checkedArduinos);
    // si annulla l'attesa
    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 visualizzati (per impostazione predefinita) nello stato in cui si trovavano al momento della progettazione della vista:

  • il [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 al momento della progettazione possono essere adeguati o meno al momento del ripristino di un frammento. Qual è la situazione in questo caso?

  • il [ListView] deve visualizzare l’elenco degli Arduino collegati. Il valore del [ListView] al momento della progettazione non può quindi essere utilizzato;
  • il [TextView] del messaggio di errore deve essere ripristinato nello stato (visibile o meno) in cui si trovava al momento del salvataggio. Il suo valore in fase di progettazione non può essere adatto a entrambi i casi;

È quindi necessario salvare lo stato di questi due componenti durante il salvataggio dello stato del frammento:

  • l’elenco degli Arduino collegati;
  • la visibilità (visualizzato/nascosto) del messaggio di errore relativo all’immissione del codice 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 {

  // visualizza il messaggio di errore
  private boolean txtMsgErreurUrlServiceRestVisible;

  // getter e setter
...
}

Compito: creare la classe [ConfigFragmentState] precedente.


Per riprodurre correttamente gli stati dei frammenti, è necessario modificare i loro metodi [getNumView] e [saveFragment]. Ad esempio, quella del frammento [BlinkFragment] è attualmente la seguente:


  @Override
  public CoreState saveFragment() {
    // è necessario salvare il frammento
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // senon c'è nulla da salvare, eseguire [return new CoreState();] ed eliminare la classe [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // è necessario restituire il numero del frammento nella tabella dei frammenti gestiti dall'attività (cfr. MainActivity)
    return 0;
}

Se non si interviene, lo stato restituito alla riga 6 verrà salvato nell’elemento 0 (riga 13) dell’array CoreState[] coreStates della classe [AbstractSession] (riga 5 qui sotto):


public class AbstractSession implements ISession {
  ...

  // stato delle viste
  private CoreState[] coreStates = new CoreState[0];
...

Ma dovrebbe essere salvato nell’elemento corrispondente al n. del frammento [BlinkFragment] nella tabella dei frammenti definita 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_()};
  }


I numeri dei frammenti sono stati definiti nell’interfaccia [IMainActivity]:


public interface IMainActivity extends IDao {

  ...

  // numeri delle viste
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}

In definitiva, lo stato del frammento [BlinkFragment] verrà gestito correttamente se si scrive:


  @Override
  public CoreState saveFragment() {
    // è necessario salvare il frammento
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // senon c'è nulla da salvare, eseguire [return new CoreState();] ed eliminare la classe [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // è necessario restituire il numero del frammento nella tabella dei frammenti gestiti dall'attività (cfr. MainActivity)
    return IMainActivity.VUE_BLINK;
}
  • riga 14: viene restituito il numero del frammento [BlinkFragment] nella tabella dei frammenti gestiti dall'attività;

Inoltre, la classe [CoreState], che fa da capostipite agli stati dei frammenti, è attualmente la seguente (cfr. paragrafo 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 {
  // frammento visitato o meno
  protected boolean hasBeenVisited = false;
  // stato dell'eventuale menu del frammento
  protected MenuItemState[] menuOptionsState;

  // getter e setter
....
}
  • righe 12-16: la classe [DummyFragmentState] non figura nell’elenco delle classi figlie della classe [CoreState]. Tuttavia, il metodo [saveFragment] della classe [BlinkFragment] restituisce attualmente un tipo [ DummyFragmentState]. Se si lascia la situazione così com’è, la serializzazione/deserializzazione della sessione fallirà e la sessione non verrà ripristinata, causando un arresto anomalo dell’applicazione;

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


  @Override
  public CoreState saveFragment() {
    // è necessario salvare il frammento
    BlinkFragmentState state=new BlinkFragmentState();
    // ...
    return state;
    // senon c'è nulla da salvare, eseguire [return new CoreState();] ed eliminare la classe [DummyFragmentState]
}

Operazione: in ciascuno dei frammenti, modificare 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 sopra).


5.6.10.7. Gestione del ciclo di vita del frammento

Ci occupiamo qui del ciclo di vita del frammento [ConfigFragment], in particolare dei quattro metodi:

  • [saveFragment]: deve salvare lo stato del frammento in modo che possa essere ripristinato in un secondo momento;
  • [initFragment]: che deve inizializzare alcuni campi del frammento, se necessario. Questo metodo viene chiamato all’avvio dell’applicazione e ogni volta che si verifica una rotazione del dispositivo. Più precisamente, viene chiamato quando il frammento diventa visibile dopo uno dei due eventi precedenti;
  • [initView]: che deve inizializzare alcuni componenti della vista, se necessario. Questo metodo viene chiamato ogni volta che è stato chiamato [initFragment] e quando la vista deve essere rigenerata perché il frammento, in un determinato momento, è uscito dall’adiacenza del frammento visualizzato. Come in precedenza, viene chiamato quando il frammento diventa visibile in seguito a uno di questi eventi;
  • [updateOnRestore]: viene eseguito dopo i due metodi precedenti in caso di rotazione del dispositivo, ma anche in caso di navigazione. Il suo ruolo è quello di ripristinare lo stato precedente del frammento;

Questi metodi saranno i seguenti:


// adattatore dell'elenco degli Arduino
  private ListArduinosAdapter adapterListArduinos;

...
  // gestione del ciclo di vita del frammento -------------------------------------

  @Override
  public CoreState saveFragment() {
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtMsgErreurUrlServiceRestVisible(txtMsgErreurUrlServiceRest.getVisibility() == View.VISIBLE);
    return state;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // adattatore listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);

  }

  @Override
  protected void initView(CoreState previousState) {
    // collegamento listview / adattatore
    listArduinos.setAdapter(adapterListArduinos);
    // Prima visita?
    if (previousState == null) {
      // ListView vuoto - creato da [initFragment]
      // messaggio di errore nascosto
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    } else {
      // si ripristina la visibilità del messaggio di errore
      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() {
    // pulsanti
    initButtons();
}
  • riga 2: l’adattatore del ListView degli Arduino. È una variabile globale perché utilizzata in diversi metodi;
  • righe 7-12: il metodo [saveFragment] salva in un tipo [ConfigFragmentState] la visibilità di TextView e txtMsgErreurUrlServiceRestVisible (riga 10);
  • righe 14-19: il metodo [initFragment] inizializza l’adattatore della riga 2 con l’elenco degli Arduino presenti nella sessione (riga 17). Si ricorda che il ruolo di [initFragment] è quello di inizializzare i campi del frammento. In questo caso l’inizializzazione deve essere eseguita in ogni caso, sia alla prima visita (previousState==null) che in caso contrario;
  • riga 17: si nota che l’adattatore è collegato alla fonte di dati [session.getCheckedArduinos]. È necessario che questa non abbia il valore null. Per questo motivo, il campo [session.checkedArduinos] viene inizializzato con un elenco vuoto nella sessione:

  // l'elenco degli Arduino
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
  • righe 21-35: il metodo [initView] ha il compito di inizializzare alcuni componenti dell’interfaccia visiva, in particolare quelli il cui valore non viene mantenuto durante la rotazione del dispositivo;
  • riga 24: il ListView degli Arduino è associato all'adattatore della linea 2;
  • righe 28-32: si distingue la prima visita dalle altre visite;
  • riga 29: durante la prima visita, deve essere visualizzato un [ListView] vuoto. È così poiché, durante la prima visita, l'adattatore del [ListView] è stato associato a un elenco vuoto (riga 17);
  • riga 31: il messaggio di errore è nascosto;
  • righe 32-36: il caso in cui non si tratti della prima visita;
  • il [ListView] è già nello stato corretto dalla riga 24. Non c'è altro da fare;
  • righe 34-35: si ripristina il messaggio di errore allo stato in cui si trovava all’ultimo salvataggio del frammento;
  • righe 31-36: il metodo [updateOnRestore] deve riportare il frammento al suo stato iniziale. Si arriva al metodo [updateOnRestore] in due modi:
    • o perché si è verificata una rotazione del dispositivo. In questo caso, tutte le inizializzazioni necessarie sono state effettuate in [initView];
    • oppure perché si naviga da una scheda alla scheda [Config]. Se il frammento [Config] è uscito dall'adiacenza dei frammenti visualizzati da quando è stato abbandonato, il metodo [initView] è stato quindi eseguito e il frammento si trova già nello stato desiderato. Se il frammento [Config] non è uscito dall'adiacenza dei frammenti visualizzati da quando è stato abbandonato, i suoi componenti visivi non hanno cambiato stato e non c'è nulla da fare;

Si nota che il metodo [updateOnRestore] non ha nulla da fare. A volte è così, altre volte no. La differenza sta nel metodo [updateOnSubmit]: se questo metodo esegue un'operazione che rende superflue alcune inizializzazioni effettuate in [initView], allora tali inizializzazioni dovrebbero essere effettuate nel metodo [updateOnRestore]. Prendiamo l’esempio di un pulsante di opzione con tre valori: V1, V2, 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] è inutile, poiché nel caso di un [SUBMIT], tale valore verrà sostituito da quello fornito dal metodo [updateOnSubmit]. È quindi preferibile spostare tale ripristino nel metodo [updateOnRestore] per evitare di eseguire, in alcuni casi, un'operazione superflua.

  • righe 48-52: il metodo [notifyEndOfUpdates] viene eseguito dopo tutti i precedenti;
  • riga 51: i pulsanti vengono riportati al loro stato iniziale: pulsante [Rafraîchir] visualizzato, pulsante [Annuler] nascosto:

Esercizio: inserite il codice precedente in [ConfigFragment], quindi eseguite l’applicazione. Verificate che, quando ruotate il dispositivo, la scheda [Config] mantenga il proprio stato (messaggio di errore, elenco degli Arduino). Verificate che lo stesso avvenga quando si esegue una semplice navigazione dalla scheda [config] alla scheda [Commands] e poi alla scheda [Config]. In quest'ultimo caso, se in [IMainActivity] è stata mantenuta un'adiacenza di frammenti pari a 1, allora la vista del frammento [ConfigFragment] viene distrutta quando si passa alla scheda [Commands] e poi ricreata quando si ritorna alla scheda [Config]. Durante i test, esaminare i log.


5.6.10.8. Miglioramento del codice

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


// adattatore dell'elenco degli Arduino
  private ListArduinosAdapter adapterListArduinos;

...

  // visualizzazione dell'elenco degli Arduino
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // visualizzazione degli Arduino
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
  }

  // azzeramento dell'elenco degli Arduino
  private void clearArduinos() {
    // visualizza un elenco vuoto
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
  • si nota che alle righe 9 e 16 viene utilizzata una variabile locale scollegata dal campo della riga 2, mentre in realtà si tratta della stessa entità che si desidera manipolare;

Modifichiamo il codice come segue:


  // adattatore dell'elenco degli Arduino
  private ListArduinosAdapter adapterListArduinos;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  ...
  }

  private void getArduinosInBackground() {
 ...
    // la si consuma
    consumeArduinosResponse(response);
  }

  // visualizzazione della risposta
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // errore?
    if (response.getStatus() != 0) {
      // visualizzazione
      showAlert(response.getMessages());
      // annullamento
      doAnnuler();
      // ritorno all'interfaccia utente
      return;
    }
    // si crea un elenco di [CheckedArduino]
    List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
    checkedArduinos.clear();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // li visualizziamo
    adapterListArduinos.notifyDataSetChanged();
    // si annulla l'attesa
    cancelWaitingTasks();
}
  
  @Override
  protected void initFragment(CoreState previousState) {
    // adattatore listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);

  }

  @Override
  protected void initView(CoreState previousState) {
    // collegamento tra ListView e adattatore
    listArduinos.setAdapter(adapterListArduinos);
    ...
}
  • quando viene eseguito il metodo della riga 5, il ciclo di vita del frammento è già stato completato. Quindi:
    • l'adattatore della riga 2 è stato associato alla sua fonte di dati (riga 41);
    • il [ListView] degli Arduino collegati è stato collegato a questo adattatore (riga 48);

Quando si desidera modificare la visualizzazione del [ListView], è necessario eseguire due operazioni:

  • modificare il contenuto della fonte dati [session.checkedArduinos];
  • segnalare questa modifica all’adattatore tramite l’istruzione [adapterListArduinos.notifyDataSetChanged()];

Si tratta proprio di modificare il contenuto della fonte dati e non la fonte dati stessa. Se si modifica la fonte dati stessa, l’operazione [adapterListArduinos.notifyDataSetChanged()] continuerà a visualizzare la vecchia fonte dati. Sarebbe quindi necessario associare l’adattatore alla nuova fonte dati.

Il codice è il seguente:

  • riga 27: recuperiamo la fonte dati;
  • riga 28: la svuotiamo. Per questo motivo, abbiamo eliminato il metodo [clearArduinos];
  • righe 29-31: in questo elenco ormai vuoto, aggiungiamo nuovi elementi;
  • riga 33: si indica all'adattatore di aggiornarsi. Ciò aggiornerà la visualizzazione del [ListView] associato;

Esercizio: apportate queste modifiche e verificate che la vostra 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]. Cominciamo con la vista [blink.xml]. Mentre prima non visualizzava nulla, ora mostrerà l’elenco degli Arduino connessi:

Image

 

Il codice XML della 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 ripreso direttamente dalla vista [config.xml]. È stato semplicemente modificato il margine superiore della 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] subisce delle modifiche:

  

  // componenti visivi
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;

  // adattatore dell'elenco degli Arduino
  private ListArduinosAdapter adapterListArduinos;
...

  // metodi imposti dalla classe padre -------------------------------------------------------

...
  @Override
  protected void initFragment(CoreState previousState) {
    // adattatore listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), true);

  }

  @Override
  protected void initView(CoreState previousState) {
    // collegamento tra ListView e adattatore
    listArduinos.setAdapter(adapterListArduinos);
  }
...
  • righe 2-3: il componente [ListView] degli Arduino collegati;
  • riga 6: l'adattatore di questo [ListView];
  • righe 12-23: il codice dei metodi [initFragment] e [initView] è quello già utilizzato per il frammento [ConfigFragment];
  • riga 15: quando il frammento deve essere reinizializzato, si reinizializza l’adattatore della riga 2 associandolo all’elenco degli Arduino memorizzato nella sessione. L'ultimo parametro [true] del costruttore [ListArduinosAdapter] indica che si desidera visualizzare una casella di controllo accanto a ciascun Arduino;
  • riga 22: quando la visualizzazione del frammento deve essere reinizializzata, si associa il [ListView] degli Arduino collegati all'adattatore della riga 6;

Esercizio: Duplicate questo codice negli altri frammenti [CommandsFragment, PinReadFragment, PinWriteFragment]. Eseguite l’applicazione e verificate ora che ogni scheda visualizzi l’elenco degli Arduino collegati. Verificate inoltre che, se selezionate degli Arduino in una scheda e passate a un’altra scheda, li ritroviate selezionati anche in quest’ultima.


Nota: la spiegazione del mantenimento degli Arduino selezionati è la seguente. La classe [ListArduinosAdapter] è stata presentata al paragrafo 5.6.10.4. Il codice relativo alla casella di selezione è il seguente:


        // l'Arduino corrente
        final CheckedArduino arduino = arduinos.get(position);
...
        // il CheckBox non è sempre visibile
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // gli si assegna il valore
            ck.setChecked(arduino.isChecked());
            // si gestisce il clic
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
}
  • righe 11-15: se nella scheda X si seleziona una casella, la proprietà [checked] dell’Arduino della riga 2 viene impostata su true (riga 14);
  • quando si passa alla scheda Y, viene visualizzato il valore [ListView] degli Arduino presenti in quella scheda. Alla riga 9, si vede che se la proprietà [checked] dell’Arduino della riga 2 viene impostata su true, allora la casella [ck] della riga 5 verrà spuntata;

5.6.12. Il livello [DAO]

Nota: per questa parte, rivedere l’implementazione del livello [DAO] nel progetto [exemple-16B] (cfr. paragrafo 2.8.3).

Finora abbiamo generato manualmente l’elenco degli Arduino collegati. Ora lo richiederemo al server web / jSON. A tal fine, 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 {
  // URL del servizio web
  void setUrlServiceWebJson(String url);

  // utente
  void setUser(String user, String mdp);

  // timeout del client
  void setTimeout(int timeout);

  // autenticazione di base
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modalità debug
  void setDebugMode(boolean isDebugEnabled);

  // Tempo di attesa del client in millisecondi prima della richiesta
  void setDelay(int delay);

  // specifico ----------------------------------------
  // elenco degli Arduino
  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] consente di ottenere l’elenco degli Arduino connessi sotto forma di un osservabile di tipo Observable<[Response<List<Arduino>>>];

Si ricorda 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> {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // eventuali messaggi di errore
    private List<String> messages;
    // il corpo della risposta
    private T body;

    // costruttori
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getter e setter
...
}

5.6.12.2. L'interfaccia [WebClient]

  

L'interfaccia [WebClient] è un'interfaccia la cui implementazione è fornita dalla libreria AA. 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);

  // specifico --------------------------------------
  // elenco degli Arduino
  @Get("/arduinos")
  Response<List<Arduino>> getArduinos();
}
  • righe 15-19: queste righe sono presenti di default nell'interfaccia [WebClient] del progetto modello [client-android-skel];
  • riga 23: l’URL del server che consente di ottenere l’elenco degli Arduino tramite un’operazione GET. Si ricorda che questo URL è misurato rispetto all’URL radice [RestClientRootUrl] della riga 16;
  • riga 24: il server restituisce la stringa jSON di tipo [Response<List<Arduino>>]. Questa stringa jSON viene automaticamente deserializzata nel tipo [Response<List<Arduino>>] grazie al convertitore jSON [MappingJackson2HttpMessageConverter] della riga 15;

5.6.12.3. La classe [Dao]

La classe [Dao] implementa l'interfaccia [IDao] nel modo seguente:


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 {

  // client del servizio web
  @RestService
  protected WebClient webClient;
  // sicurezza
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // il RestTemplate
  private RestTemplate restTemplate;
  // factory di RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // registro
    Log.d(className, "afterInject");
    // si costruisce il restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // si imposta il convertitore jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // si imposta il restTemplate del client web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // si imposta l'URL del servizio web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // si registra l'utente nell'intercettatore
    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));
    }
    // configurazione predefinita
    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));
    }
    // intercettatore di autenticazione?
    if (isBasicAuthentificationNeeded) {
      // si aggiunge l'intercettatore di autenticazione
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // metodi privati -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // implementazione specifica IDao -----------------------------------------------

  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    // esecuzione client web
    return getResponse(new IRequest<Response<List<Arduino>>>() {
      @Override
      public Response<List<Arduino>> getResponse() {
        return webClient.getArduinos();
      }
    });
  }
}
  • righe 19-87: queste righe sono di base nella classe [Dao] del 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 della riga 92, quindi in questo caso un tipo [Response<List<Arduino>>];
  • riga 97: il metodo [IRequest.getResponse()] delega il lavoro al metodo [webClient.getArduinos()] che abbiamo presentato. [webClient], definito alla riga 24, viene istanziato dalla libreria AA ed è un'istanza dell'interfaccia [WebClient] che abbiamo presentato;

5.6.13. L'attività [MainActivity]

  

Abbiamo già presentato l'attività [MainActivity] al paragrafo 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 si aggiunge un metodo all’interfaccia [IDao], è necessario implementarlo 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 {

  // livello [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessione
  private Session session;

...

  // implementazione 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] appena presentata, di cui si trova un riferimento alla riga 8;

5.6.14. Il frammento [ConfigFragment] rivisitato

Nella classe [ConfigFragment], il codice eseguito quando si fa clic sul pulsante [Rafraîchir] è per ora il seguente:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    ...
    // si richiede l'elenco degli Arduino in background
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
    // si crea un elenco fittizio di Arduino
    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));
    }
    // si simula una risposta del server
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // la si elabora
    consumeArduinosResponse(response);
  }

  // visualizzazione della risposta
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    ...
}

Dobbiamo riscrivere le righe 10-16 che generavano in modo statico una risposta di tipo [Response<List<Arduino>>]. Dobbiamo ora richiedere questo elenco al livello [DAO] tramite l’attività. Il codice diventa il seguente:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // si verificano i dati inseriti
    if (!pageValid()) {
      return;
    }
    // si memorizza l'input
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // si prepara l'attesa
    beginWaiting(1);
    // si esegue l'attività asincrona
    executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {

      @Override
      public void call(Response<List<Arduino>> response) {
        // si elabora la risposta
        consumeArduinosResponse(response);
      }
    });
}
  • riga 8: l’URL radice del servizio web / jSON inserita dall’utente viene trasferita al livello [DAO] tramite l’attività. Questo sarà il URL radice dell'interfaccia [WebClient] (cfr. paragrafo 5.6.12.2);
  • riga 10: si avvisa la classe padre che si sta per avviare un'attività asincrona;
  • righe 12-19: avvio dell’attività asincrona che restituirà l’elenco degli Arduino connessi al server;
  • riga 12: chiamata al metodo [executeInBackground] della classe padre. Questo metodo richiede due parametri:
    • riga 12: il processo da monitorare. In questo caso, tale processo è fornito dal metodo [mainActivity.getArduinos()];
    • righe 12-19: un'istanza dell'interfaccia [Action1<T>], dove il tipo T è il tipo fornito dal processo, in questo caso un tipo [Response<List<Arduino>>];
  • righe 14-18: il metodo chiamato quando l'attività asincrona restituisce il proprio risultato di tipo [Response<List<Arduino>>];
  • riga 17: si passa la risposta ricevuta al metodo [consumeArduinosResponse] già scritto;

Esercizio: Avviare il server come indicato nel paragrafo 5.4. Collegare uno o più Arduino al PC su cui è stato avviato il server. Quindi avviare il client Android e verificare di poter ottenere correttamente l’elenco degli Arduino collegati. Osservare i log.


Image

  • digitare l'indirizzo URL indicato in [1]. Si tratta di uno degli indirizzi IP del proprio server;
  • fare clic sul pulsante [2];
  • dovresti ottenere l'elenco degli Arduino collegati in [3];

Verificate che questo elenco compaia anche nelle altre schede.

5.7. Compiti da svolgere


Procedendo come appena illustrato per la vista [Config], realizzate e testate successivamente le altre quattro viste dell’applicazione: [Blink], [PinRead], [PinWrite] e [Commands].


Le viste da realizzare sono state illustrate nel paragrafo 5.5.

Per ciascuna vista, occorre:

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

Nota 1: l'esempio da seguire è il progetto [Exemple-16B] del corso (cfr. paragrafo 2.8.3).

Nota 2: i URL da interrogare e il tipo delle loro risposte sono stati presentati al paragrafo 5.4.2.

Nota 3:

La classe [CommandsFragment] invia un elenco contenente un unico 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 {

  // dati
  private String id;
  private String ac;
  private Map<String, Object> pa;

  // costruttori
  public ArduinoCommand() {

  }

  public ArduinoCommand(String id, String ac, Map<String, Object> pa) {
    this.id = id;
    this.ac = ac;
    this.pa = pa;
  }

  // getter e setter
...
}

Nell’interfaccia [WebClient], il metodo per eseguire questo elenco di un comando sarà il seguente:


  // invio di comandi JSON
  @Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
  • riga 2: viene richiesto URL con un ordine HTTP POST;
  • riga 3: il valore inserito deve recare l’annotazione [@Body];

Nota 4: si consiglia di procedere 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. Successivamente, per ogni vista, far scorrere il dispositivo attraverso i diversi stati della vista e annotare le informazioni perse. Sono proprio queste che devono essere salvate e poi ripristinate. Verificare quindi la navigazione: quando si esce da una scheda e vi si ritorna in un secondo momento, è necessario ritrovarla nello stato in cui è stata lasciata;