Skip to content

5. TP 2 – Sterowanie płytkami Arduino za pomocą tabletu z systemem Android

Teraz nauczymy się sterować płytką Arduino za pomocą tabletu. Przykładem do naśladowania jest projekt [client-android-skel] z kursu (patrz akapit 2).

5.1. Architektura projektu

Cały projekt będzie miał następującą architekturę:

  • blok [1], serwer WWW / jSON oraz płytki Arduino zostaną wam dostarczone;
  • będziecie musieli zbudować blok [2] oraz zaprogramować tablet z systemem Android w celu komunikacji z serwerem internetowym / jSON.

5.2. Sprzęt

Do dyspozycji macie następujące elementy:

  • Arduino z rozszerzeniem Ethernet, dioda LED i czujnik temperatury;
  • moduł miniHub do wspólnego użytkowania z innym studentem;
  • kabel USB do zasilania Arduino;
  • dwa kable sieciowe do podłączenia Arduino i modułu PC do tej samej sieci prywatnej;
  • tablet z systemem Android;

5.2.1. Arduino

Oto jak połączyć ze sobą poszczególne elementy:

  • odłącz kabel sieciowy od urządzenia PC;
  • połącz urządzenie PC z Arduino za pomocą kabla sieciowego;
  • Arduino, które posiadasz, będzie już zaprogramowane. Jego adres IP będzie brzmiał [192.168.2.2]. Aby urządzenie PC mogło wykryć Arduino, należy nadać mu adres IP w sieci [192.168.2]. Urządzenia Arduino zostały zaprogramowane do komunikacji z urządzeniem o adresie IP [192.168.2.1]. Oto jak to zrobić:

Przejdź do [Panneau de configuration\Réseau et Internet\Centre Réseau et partage]:

 
  • w [1] kliknij link [réseau local];
  • w [2] kliknij przycisk [Propriétés] w sieci lokalnej;
  • w [3] kliknij właściwości [IPv4] mapy [réseau local];
  • w [4] nadaj tej karcie adres IP [192.168.2.1] oraz maskę podsieci [255.255.255.0];
  • w polu [5] kliknij [OK] tyle razy, ile potrzeba, aby zamknąć kreatora.

5.2.2. Tablet

  • za pomocą klucza Wi-Fi podłącz swoje urządzenie do sieci Wi-Fi, którą wskażemy. Zrób to samo z tabletem;
  • Sprawdź adres Wi-Fi urządzenia IP, wpisując [ipconfig] w oknie DOS. Znajdziesz adres w formacie [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
  • sprawdź adres Wi-Fi swojego tabletu (IP). Jeśli nie wiesz, jak to zrobić, zapytaj swojego opiekuna. Znajdziesz adres w formacie [192.168.x.z];
  • wyłącz zaporę sieciową na swoim urządzeniu PC, jeśli jest aktywna [Panneau de configuration\Système et sécurité\Pare-feu Windows];
  • w oknie DOS sprawdź, czy PC i tablet mogą się komunikować, wpisując polecenie [ping 192.168.x.z], gdzie [192.168.x.z] to adres IP twojego tabletu. Tablet powinien wówczas odpowiedzieć:
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

Konfiguracja sieciowa systemu jest już gotowa.

5.2.3. Emulator [Genymotion]

Emulator [Genymotion] (patrz punkt 6.9) stanowi doskonałą alternatywę dla tabletu. Działa niemal tak samo szybko i nie wymaga połączenia Wi-Fi. Zaleca się korzystanie właśnie z tej metody. Tablet można wykorzystać do końcowej weryfikacji aplikacji.

5.3. Programowanie Arduino

W tym miejscu skupiamy się na pisaniu kodu w języku C dla Arduino:

Warto przeczytać

  • instalacja środowiska programistycznego Arduino (patrz punkt 6.1);
  • korzystanie z bibliotek jSON (załączniki, punkt 6.6);
  • w środowisku programistycznym Arduino przetestowanie przykładowego serwera (np. serwera WWW) oraz przykładowego klienta (np. klienta Telnet);
  • załączniki dotyczące środowiska programistycznego Arduino w punkcie 6.1.

Arduino to zestaw pinów połączonych ze sprzętem. Piny te są wejściami lub wyjściami. Ich wartość jest binarna lub analogowa. Aby sterować Arduino, należy wykonać dwie podstawowe operacje:

  • zapisanie wartości binarnej/analogowej na pinie oznaczonym jego numerem;
  • odczytanie wartości binarnej/analogowej z pinu oznaczonego jego numerem;

Do tych dwóch podstawowych operacji dodamy trzecią:

  • sprawienie, by dioda LED migała przez określony czas i z określoną częstotliwością. Operację tę można wykonać poprzez wielokrotne wywoływanie dwóch poprzednich podstawowych operacji. Jednak podczas testów przekonamy się, że wymiana danych między modułem [DAO] a Arduino trwa około jednej sekundy. Nie jest zatem możliwe, aby dioda LED migała na przykład co 100 milisekund. Dlatego też zaimplementujemy tę funkcję migania bezpośrednio na samym Arduino.

Działanie Arduino będzie wyglądało następująco:

  • komunikacja między warstwą [DAO] a Arduino odbywa się za pośrednictwem sieci TCP-IP poprzez wymianę wierszy tekstu w formacie jSON (JavaScript Object Notation);
  • po uruchomieniu Arduino łączy się z portem 100 serwera rejestrującego znajdującego się w warstwie [DAO]. Wysyła do serwera pojedynczy wiersz tekstu:
{"id":"cuisine","desc":"duemilanove","mac":"90:A2:DA:00:1D:A7","port":102}

Jest to ciąg znaków jSON charakteryzujący łączące się urządzenie Arduino:

  • id: identyfikator Arduino;
  • desc: opis możliwości urządzenia Arduino. W tym przypadku podano po prostu typ urządzenia Arduino;
  • mac: adres MAC urządzenia Arduino;
  • port: numer portu, na którym Arduino będzie oczekiwać poleceń z warstwy [DAO].

Wszystkie te informacje mają postać ciągów znaków, z wyjątkiem portu, który jest liczbą całkowitą.

  • Gdy Arduino zarejestruje się na serwerze rejestracyjnym, zaczyna nasłuchiwać na porcie, który podało serwerowi (powyżej 102). Oczekuje na polecenia jSON o następującej postaci:
{"id":"identifiant","ac":"une_action","pa":{"param1":"valeur1","param2":"valeur2",...}}

Jest to ciąg znaków jSON zawierający następujące elementy:

  • id: identyfikator polecenia. Może być dowolny;
  • ac: akcja. Są trzy:
  • pw (pin write) – zapis wartości na pinie,
  • pr (pin read) – odczyt wartości z pinu,
  • cl (miganie) – służy do migania diody LED;
  • pa: parametry akcji. Zależą one od konkretnej akcji.
  • Arduino zawsze zwraca odpowiedź do swojego klienta. Jest to ciąg znaków jSON o następującej postaci:
{"id":"1","er":"0","et":{"pinx":"valx"}}

gdzie

  • id: identyfikator polecenia, na które udzielana jest odpowiedź;
  • er (błąd): kod błędu, jeśli wystąpił błąd, w przeciwnym razie 0;
  • oraz (stan): słownik, który jest zawsze pusty, z wyjątkiem polecenia odczytu pr. Wówczas słownik zawiera wartość z pinu nr x, o którą zapytano.

Oto przykłady mające na celu wyjaśnienie powyższych specyfikacji:

Spraw, aby dioda nr 8 migała 10 razy z okresem 100 milisekund:

Polecenie
{"id":"1","ac":"cl","pa":{"pin":"8","dur":"100","nb":"10"}}
Odpowiedź
{"id":"1","er":"0","et":{}}

Parametry polecenia cl to: czas trwania jednego mignięcia w milisekundach (dur), liczba mignięć (nb) oraz numer pinu diody LED.

Zapisz wartość binarną 1 na pinie nr 7:

Polecenie
{"id":"2","ac":"pw","pa":{"pin":"7","mod":"b","val":"1"}}
Odpowiedź
{"id":"2","er":"0","et":{}}

Parametry pa polecenia pw to: tryb zapisu mod b (binarny) lub a (analogowy), wartość val do zapisu oraz numer pinu. W przypadku zapisu binarnego wartość val wynosi 0 lub 1. W przypadku zapisu analogowego wartość val mieści się w przedziale [0,255].

Zapis wartości analogowej 120 na pinie nr 2:

Polecenie
{"id":"3","ac":"pw","pa":{"pin":"2","mod":"a","val":"120"}}
Odpowiedź
{"id":"3","er":"0","et":{}}

Odczyt wartości analogowej z pinu 0:

Polecenie
{"id":"4","ac":"pr","pa":{"pin":"0","mod":"a"}}
Odpowiedź
{"id":"4","er":"0","et":{"pin0":"1023"}}

Parametry polecenia pr to: tryb odczytu mod b (binarny) lub a (analogowy) oraz numer pinu. Jeśli nie wystąpi żaden błąd, Arduino umieszcza w części „et” swojej odpowiedzi wartość żądanego pinu. W tym przypadku „pin0” oznacza, że zapytano o wartość pinu nr 0, a 1023 to właśnie ta wartość. Podczas odczytu wartość analogowa będzie mieścić się w przedziale [0, 1024].

Przedstawiliśmy trzy polecenia: cl, pw i pr. Można się zastanawiać, dlaczego nie użyliśmy bardziej jednoznacznych pól w ciągach znaków typu jSON, np. „action” zamiast „ac”, „pinwrite” zamiast „pw”, „paramètres” zamiast „pa”... Arduino ma bardzo ograniczoną pamięć. A ciągi znaków jSON wymieniane z Arduino zajmują miejsce w pamięci. Dlatego zdecydowaliśmy się skrócić je do minimum.

Przyjrzyjmy się teraz kilku przykładom błędów:

Kod
xx
Odpowiedź
{"id":"","er":"100","et":{}}

Wysłano polecenie, które nie ma formatu jSON. Arduino zwróciło kod błędu 100.

Polecenie
{"id":"4","ac":"pr","pa":{"mod":"a"}}
Odpowiedź
{"id":"4","er":"302","et":{}}

Wysłano polecenie pr, pomijając parametr pin. Arduino zwróciło kod błędu 302.

Polecenie
{"id":"4","ac":"pinread","pa":{"pin":"0","mod":"a"}}
Odpowiedź
{"id":"4","er":"104","et":{}}

Wysłano nieznane polecenie pinread (czyli pr). Arduino zwróciło kod błędu 104.

Nie będziemy kontynuować przykładów. Zasada jest prosta. Arduino nie może się zawiesić, niezależnie od tego, jakie polecenie zostanie mu wysłane. Przed wykonaniem polecenia jSON upewnia się, że jest ono poprawne. Gdy tylko pojawi się błąd, Arduino przerywa wykonywanie polecenia i zwraca klientowi ciąg błędu jSON. Również w tym przypadku, ze względu na ograniczenia pamięci, zwracany jest kod błędu zamiast pełnego komunikatu.

Kod programu uruchamianego na Arduino znajduje się w przykładach zawartych w niniejszym dokumencie:

  

Aby przenieść go na Arduino:

  • podłącz je do swojego PC;
  • w [1], otwórz plik [arduino_uno.ino]. Arduino IDE uruchomi się i załaduje plik;

Uwaga: kod został pierwotnie stworzony i przetestowany z wersją IDE ARDUINO 1.5.x. Od tego czasu pojawiły się inne wersje IDE. Kod nie działał z wersją IDE ARDUINO 1.6.x. Wygląda na to, że występuje problem z kompatybilnością wsteczną między wersjami 1.6 i 1.5.

  • W przypadku [2-4] należy podać typ używanego Arduino;
  • w pliku [5-7] należy wskazać, na którym porcie szeregowym urządzenia PC się znajduje;
  • w pliku [8] należy wgrać (=załadować) program [arduino_uno] na Arduino;

Kod programu jest bogato opatrzony komentarzami. Zainteresowani czytelnicy mogą się z nim zapoznać. Zwracamy jedynie uwagę na linie kodu, które umożliwiają skonfigurowanie dwukierunkowej komunikacji klient–serwer między Arduino a PC:


#include <SPI.h>
#include <Ethernet.h>
#include <ajSON.h>

// ---------------------------------- CONFIGURATION DE L'ARDUINO UNO
// adres MAC Arduino UNO
byte macArduino[] = { 
  0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// adres Arduino IP
IPAddress ipArduino(192,168,2,2);
// jego identyfikator
char * idArduino="cuisine";
// port serwera Arduino
int portArduino=102;
// opis Arduino
char * descriptionArduino="contrôle domotique";
// serwer Arduino będzie działał na porcie 102
EthernetServer server(portArduino);
// IP serwera rejestrującego
IPAddress ipServeurEnregistrement(192,168,2,1); 
// port serwera rejestrującego
int portServeurEnregistrement=100;
// klient Arduino serwera rejestrującego
EthernetClient clientArduino;
// polecenie klienta
char commande[100];
// odpowiedź z Arduino
char message[100];

// inicjalizacja
void setup() {
  // Monitor szeregowy umożliwi śledzenie wymiany danych
  Serial.begin(9600);
  // uruchomienie połączenia Ethernet
  Ethernet.begin(macArduino,ipArduino);  
  // dostępna pamięć
  Serial.print(F("Memoire disponible : "));
  Serial.println(freeRam());
}

// pętla nieskończona
void loop()
{
  ...
}
  • wiersz 8: adres MAC Arduino. Nie ma on tutaj większego znaczenia, ponieważ Arduino będzie znajdować się w sieci prywatnej, w której znajduje się moduł PC oraz jedno lub więcej urządzeń Arduino. Wystarczy, aby adres MAC był unikalny w tej sieci prywatnej. Zazwyczaj na karcie sieciowej Arduino znajduje się naklejka z podanym adresem MAC karty. Jeśli tej naklejki nie ma i nie znasz adresu MAC karty, w wierszu 8 możesz wpisać dowolną wartość, o ile zachowana jest zasada unikalności adresu MAC w sieci prywatnej;
  • wiersz 11: adres karty IP. Ponownie można wpisać dowolną wartość typu [192.168.2.x], zmieniając wartość x dla poszczególnych modułów Arduino w sieci prywatnej;
  • wiersz 13: identyfikator Arduino. Musi być unikalny wśród identyfikatorów urządzeń Arduino w tej samej sieci prywatnej;
  • wiersz 15: port serwisowy Arduino. Można wpisać dowolną wartość;
  • wiersz 17: opis funkcji Arduino. Można wpisać dowolną wartość. Należy uważać na długie ciągi znaków ze względu na ograniczoną pamięć Arduino;
  • wiersz 21: adres IP serwera rejestrującego Arduino na serwerze PC. Nie wolno go zmieniać;
  • wiersz 23: port tej usługi rejestrującej. Nie należy go zmieniać;

5.4. Serwer WWW / jSON

5.4.1. Instalacja

Image

Otrzymano plik binarny Java serwera WWW / jSON:

 

Otwórz okno wiersza poleceń i wpisz następujące polecenie:

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

Jeśli plik [java.exe] nie znajduje się w katalogu PATH w oknie poleceń, konieczne będzie wpisanie pełnej ścieżki do pliku [java.exe] (zazwyczaj C:\Program Files\java\...).

Otworzy się okno DOS i wyświetli logi:


.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 [/**] do modułu obsługi typu [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/**] do obsługi typu [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
  • wiersz 11: uruchomiono wbudowany serwer Tomcat;
  • wiersz 15: serwlet [dispatcherServlet] z biblioteki Spring MVC jest ładowany i uruchamiany;
  • wiersz 18: wykryto serwlet REST URL ([/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}]);
  • wiersz 19: wykryto serwlet REST URL ([/arduinos/commands/{idArduino}]);
  • wiersz 20: wykryto URL Rest [/arduinos/];
  • wiersz 21: wykryto URL Rest [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}];
  • wiersz 22: wykryto URL Rest [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}];
  • wiersz 26: uruchomiono serwer rejestrujący urządzenia Arduino;

Podłącz Arduino do urządzenia PC, jeśli jeszcze tego nie zrobiłeś. Zapora sieciowa urządzenia PC musi być wyłączona. Następnie za pomocą przeglądarki wywołaj adres URL [http://localhost:8080/arduinos]:

Powinien pojawić się identyfikator podłączonego Arduino. Jeśli nic się nie wyświetla, spróbuj zresetować Arduino. Posiada ono w tym celu przycisk.

Serwer WWW / jSON jest teraz zainstalowany.

5.4.2. Dane URL udostępniane przez serwis internetowy / jSON

Warto przeczytać: projekt [Exemple-15] (patrz punkt 1.16.1);

Serwis internetowy / jSON został zaimplementowany przy użyciu Spring MVC i udostępnia następujące URL:


@Controller
public class WebController {

  // warstwa biznesowa
  @Autowired
  private IMetier métier;

  // lista urządzeń Arduino
  @RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String getArduinos() throws JsonProcessingException {
    ...
  }

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

  // wysyłanie poleceń 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 {
    ...
  }

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

  // zapis na pinie
  @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 {
  ...
  }
}

Odpowiedzi wysyłane przez serwer są reprezentacjami jSON następującej klasy [Response<T>]:


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- właściwości
    // status operacji
    private int status;
    // ewentualne komunikaty o stanie
    private List<String> messages;
    // treść odpowiedzi
    private T body;

    // konstruktory
    public Response() {

    }

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

    // metody pobierające i ustawiające
...
}

URL [/arduinos] wysyła odpowiedź typu [Response<List<Arduino>>], gdzie [Arduino] to następująca klasa:


package android.arduinos.entities;

import java.io.Serializable;

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

// metody pobierające i ustawiające
...
}
  • wiersz 7: [id] to identyfikator Arduino;
  • wiersz 8: jego opis;
  • wiersz 9: jego adres MAC;
  • wiersz 10: jego adres IP;
  • wiersz 11: port, na którym oczekuje poleceń;

URL:

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

wysyłają odpowiedź typu [Response<ArduinoResponse>], gdzie klasa [ArduinoResponse] reprezentuje standardową odpowiedź Arduino:


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

  // metody pobierające i ustawiające
...
}
  • [json]: ciąg znaków jSON wysłany przez Arduino, którego nie udało się zdekodować (błąd), w przeciwnym razie null;
  • [id]: identyfikator polecenia, na które odpowiada Arduino;
  • [erreur]: kod błędu, 0, jeśli OK, w przeciwnym razie inna wartość;
  • [etat]: słownik zawierający konkretną odpowiedź na polecenie. Najczęściej jest pusty, chyba że polecenie wymagało odczytania wartości z Arduino – w takim przypadku wartość ta zostanie umieszczona w tym słowniku;

5.4.3. Testy serwisu internetowego / jSON

Zapoznaj się z serwerem internetowym / jSON, testując następujące 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

Oto kilka zrzutów ekranu przedstawiających oczekiwany wynik:

Pobieranie listy podłączonych urządzeń Arduino:

Ciąg znaków jSON otrzymany z serwera WWW / jSON to obiekt zawierający następujące pola:

  • [status]: wartość 0 oznacza, że nie wystąpił błąd – w przeciwnym razie błąd wystąpił;
  • [messages]: lista komunikatów wyjaśniających błąd, jeśli błąd wystąpił:
  • [body]: lista modułów Arduino, jeśli nie wystąpił błąd. Każdy moduł Arduino jest wówczas opisany przez obiekt zawierający następujące pola:
    • [id]: identyfikator urządzenia Arduino. Dwa urządzenia Arduino nie mogą mieć tego samego identyfikatora;
    • [description]: krótki opis funkcji urządzenia Arduino;
    • [mac]: adres MAC urządzenia Arduino;
    • [ip]: adres IP urządzenia Arduino;
    • [port]: port, na którym oczekuje poleceń;

Spraw, aby dioda LED na pinie nr 8 Arduino o identyfikatorze [cuisine] migała 20 razy co 100 ms:

 

Ciąg znaków jSON otrzymany z serwera WWW / jSON jest obiektem zawierającym następujące pola:

  • [status]: wartość 0 oznacza, że nie wystąpił błąd – w przeciwnym razie błąd wystąpił;
  • [messages]: lista komunikatów wyjaśniających błąd, jeśli wystąpił błąd:
  • [body]: odpowiedź Arduino, jeśli nie wystąpił błąd:
    • [id]: identyfikator polecenia. Identyfikator ten to cyfra 1 w [/blink/1]. Arduino powtarza ten identyfikator polecenia w swojej odpowiedzi;
    • [erreur]: numer błędu. Wartość inna niż 0 oznacza błąd;
    • [etat]: używany wyłącznie do odczytu stanu pinu. Jego wartością jest wówczas wartość tego pinu;
    • [json]: używane wyłącznie w przypadku wystąpienia błędu jSON między klientem a serwerem. Jego wartością jest wówczas błędny ciąg znaków jSON wysłany przez Arduino;

Odczyt analogowy z pinu nr 0 Arduino oznaczonego jako [cuisine]:

 

Ciąg znaków jSON otrzymany z serwera WWW / jSON jest analogiczny do poprzedniego, z tą różnicą, że pole [etat] reprezentuje wartość pinu nr 0.

Odczyt binarny pinu nr 5 Arduino oznaczonego jako [cuisine]:

 

Ciąg znaków jSON otrzymany z serwera WWW / jSON jest analogiczny do poprzedniego.

Zapis binarny wartości 1 na pinie nr 8 Arduino o identyfikatorze [cuisine]:

 

Ciąg znaków jSON otrzymany z serwera internetowego / jSON jest analogiczny do poprzedniego.

Test URL [http://localhost:8080/arduinos/commands/cuisine] jest bardziej skomplikowany. Metoda serwera WWW / jSON, która przetwarza ten ciąg URL, oczekuje żądania POST, którego nie da się łatwo zasymulować za pomocą przeglądarki. Aby przetestować ten URL, można użyć przeglądarki Chrome z rozszerzeniem [Advanced REST Client] (patrz punkt 6.13):

 
  • w [1], URL metody internetowej / jSON, którą chcemy przetestować;
  • w [2] – metodę POST do wysłania żądania;
  • w [3-4] wartość wysłana to jSON;
  • w [5] wysłano ciąg znaków jSON. Należy zwrócić uwagę na nawiasy kwadratowe, które rozpoczynają i kończą listę. W tym przypadku na liście znajduje się tylko jedno polecenie jSON, które powoduje miganie pinu nr 8, 10 razy co 100 ms;
  • w pliku [6] wysyłane jest żądanie;
 
  • w [7] – odpowiedź jSON wysłana przez serwer. Obiekt otrzymał obiekt zawierający dwa standardowe pola [status, messages] oraz pole [body], którego wartością jest lista odpowiedzi Arduino na każde z wysłanych poleceń jSON.

Zobaczmy, co się dzieje, gdy wyślemy polecenie jSON, które jest niepoprawne pod względem składniowym dla Arduino:

Otrzymujemy wówczas następującą odpowiedź:

 

Widać, że w odpowiedzi z Arduino numer błędu to [104], co oznacza, że polecenie [xx] nie zostało rozpoznane.

5.5. Testy klienta na systemie Android

Oto gotowy plik wykonywalny klienta dla systemu Android:

  

Za pomocą myszki przeciągnij powyższy plik wykonywalny [app-debug.apk] na emulator tabletu [GenyMotion]. Zostanie on wówczas zapisany, a następnie uruchomiony. Uruchom również serwer WWW / jSON, jeśli jeszcze tego nie zrobiłeś. Podłącz Arduino do PC z diodą LED na obudowie. Klient na Androida umożliwia zdalne zarządzanie urządzeniami Arduino. Wyświetla on użytkownikowi następujące ekrany.

Zakładka [CONFIG] umożliwia połączenie się z serwerem i pobranie listy podłączonych urządzeń Arduino:

Image

  • w polu [1] należy wpisać adres IP [192.168.2.1] przypisany do urządzenia PC (patrz punkt 5.2).

Zakładka [PINWRITE] umożliwia zapisanie wartości na pinie Arduino:

Image

Image

Zakładka [PINREAD] umożliwia odczytanie wartości z pinu Arduino:

Image

Zakładka [BLINK] umożliwia miganie diody LED w Arduino:

Image

Zakładka [COMMAND] umożliwia wysłanie polecenia jSON do Arduino:

Image

5.6. Klient Android dla usługi internetowej / jSON

Przechodzimy teraz do tworzenia klienta na Androida.

5.6.1. Architektura klienta

Architektura klienta na Androida będzie zgodna z projektem [Exemple-15] (patrz punkt 1.16.2);

  • warstwa [DAO] komunikuje się z serwerem internetowym / jSON;

Klient Android musi mieć możliwość sterowania kilkoma modułami Arduino jednocześnie. Na przykład chcemy, aby dwie diody LED umieszczone na dwóch modułach Arduino migały jednocześnie, a nie jedna po drugiej. Dlatego nasz klient Android będzie wykorzystywał jedno zadanie asynchroniczne na każdy moduł Arduino, a zadania te będą wykonywane równolegle.

5.6.2. Projekt klienta w Android Studio

Skopiuj projekt [client-android-skel] (patrz punkt 2) do projektu [client-arduinos-01] (w razie potrzeby zapoznaj się z instrukcją kopiowania projektu Gradle w punkcie 1.15):

Image

5.6.3. Pięć widoków XML

  

Będzie pięć widoków XML:

  • [blink]: do migania diody LED w Arduino. Jest ona powiązana z fragmentem [BlinkFragment];
  • [commands]: do wysłania polecenia jSON do Arduino. Jest ona powiązana z fragmentem [CommandsFragment];
  • [config]: służy do skonfigurowania URL usługi internetowej / jSON oraz uzyskania początkowej listy podłączonych urządzeń Arduino. Jest powiązana z fragmentem [ConfigFragment];
  • [pinread]: służy do odczytu wartości binarnej lub analogowej z pinu Arduino. Jest powiązany z fragmentem [PinReadFragment];
  • [pinwrite]: służy do zapisania wartości binarnej lub analogowej na pinie Arduino. Jest powiązana z fragmentem [PinWriteFragment];

Na razie wszystkie pięć widoków XML będzie miało tę samą pustą treść:


<?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>
  • widok znajduje się w kontenerze [RelativeLayout] (wiersze 7–10), który z kolei jest zawarty w kontenerze [ScrollView] (wiersze 2–11). Dzięki temu możemy przewijać widok, jeśli jego rozmiar przekracza rozmiar ekranu tabletu;

Zadanie: utwórz pięć widoków o nazwach XML.


5.6.4. Menu fragmentów

Wiemy, że fragmenty projektu utworzonego przy użyciu [client-android-skel] muszą być powiązane z menu, nawet jeśli jest ono puste. W tym przypadku aplikacja nie będzie miała menu. Puste menu znajduje się już w projekcie;

  

5.6.5. Pięć fragmentów aplikacji

 

Zadanie: skopiuj fragment [DummyFragment] do wszystkich pięciu fragmentów aplikacji, tak jak pokazano w [2].


Fragment [ConfigFragment] ma następujący szkielet:


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 {

  // pola odziedziczone po klasie nadrzędnej -------------------------------------------------------
...

Zastąp wiersz 10 następującym wierszem:


@EFragment(R.layout.config)

Zadanie: wykonaj tę samą czynność dla pozostałych czterech fragmentów, dostosowując atrybut [@EFragment] klasy.


Fragment
Widok
ConfigFragment

R.layout.config
PinReadFragment

R.layout.pinread
PinWriteFragment

R.layout.pinwrite
CommandsFragment

R.layout.commands
BlinkFragment

R.layout.blink

5.6.6. Stany fragmentów

Każdy fragment będzie miał swój stan.


Zadanie: skopiuj klasę [DummyFragmentState] pięć razy, aby utworzyć pięć stanów przedstawionych w [2].


5.6.7. Dostosowanie projektu

 

Pakiet [architecture / custom] zawiera elementy architektury aplikacji, które można dostosować.

5.6.7.1. Interfejs [IMainActivity]

Interfejs [IMainActivity] określa, o co fragmenty mogą prosić aktywność, a także stałe aplikacji. Interfejs ten będzie wyglądał następująco:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // dostęp do sesji
  ISession getSession();

  // zmiana widoku
  void navigateToView(int position, ISession.Action action);

  // zarządzanie oczekiwaniem
  void beginWaiting();

  void cancelWaiting();

  // stałe aplikacji -------------------------------------

  // tryb debugowania
  boolean IS_DEBUG_ENABLED = true;

  // maksymalny czas oczekiwania na odpowiedź serwera
  int TIMEOUT = 1000;

  // czas oczekiwania przed wykonaniem żądania klienta
  int DELAY = 000;

  // uwierzytelnianie podstawowe
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // sąsiedztwo fragmentów
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // pasek kart
  boolean ARE_TABS_NEEDED = true;

  // obrazek oczekiwania
  boolean IS_WAITING_ICON_NEEDED = true;

  // liczba fragmentów
  int FRAGMENTS_COUNT = 5;

  // liczba wyświetleń
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}
  • wiersze 25, 28, 31, 40: konfiguracja warstwy [DAO]. Aplikacja ta wysyła zapytania do serwera WWW / jSON;
  • wiersz 37: ta aplikacja posiada zakładki;
  • wiersz 43: ta aplikacja ma pięć fragmentów;
  • wiersze 46–50: numery pięciu fragmentów;
  • wiersz 34: sąsiedztwo fragmentów. Programista może wpisać tutaj wartość z przedziału [1, FRAGMENTS_COUNT-1];

5.6.7.2. Klasa [CoreState]

Klasa [CoreState] jest klasą nadrzędną stanów fragmentów:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // fragment odwiedzony lub nie
  protected boolean hasBeenVisited = false;
  // stan ewentualnego menu fragmentu
  protected MenuItemState[] menuOptionsState;

  // metody pobierające i ustawiające
...
}
  • wiersze 12–16: należy tutaj zadeklarować klasy stanów pięciu fragmentów;

5.6.8. Klasa [MainActivity]

  

Klasa [MainActivity] będzie wyglądać następująco:


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 {

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

  // metody klasy nadrzędnej -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sesja
    this.session = (Session) super.session;
    // utworzenie pięciu zakładek
    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) {
    // wyświetlanie fragmentu o numerze pozycji
    navigateToView(position, ISession.Action.NAVIGATION);
  }

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

  // implementacja IDao -----------------------------------------
}
  • wiersze 46–50: utworzenie pięciu zakładek aplikacji;
  • wiersz 48: nazwy zakładek są generowane przez metodę z wierszy 63–79;
  • pięć fragmentów jest instancjonowanych w wierszu 60. Ze względu na adnotacje AA klasy fragmentów są takie same jak przedstawione wcześniej, z dodanym na końcu znakiem podkreślenia;
  • wiersze 63–79: definiuje się tytuł dla każdego z fragmentów. Tytuły te będą wyszukiwane w pliku [res / values / strings.xml]
  

Zawartość pliku [strings.xml] jest następująca:


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

  <!-- nazwa aplikacji -->
  <string name="app_name">[arduinos-client-01]</string>
  <!-- Fragmenty i zakładki -->
  <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>

Zadanie: utwórz powyższe elementy i skompiluj projekt. Nie powinno być żadnych błędów.


Uruchom projekt. Na emulatorze powinien pojawić się następujący widok:

Image

Przejrzyj logi, które pojawiły się podczas wyświetlania pierwszego widoku, i prześledź poszczególne wykonane etapy. Przełączaj się między kartami i kontynuuj śledzenie logów.

5.6.9. Widok XML [config]

Widok XML [config] będzie wyglądał następująco:

Powyższy widok uzyskano za pomocą następującego kodu 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>

Widok wykorzystuje ciągi znaków (android:text w wierszach 15, 25, 37, 50, 61, 73), które są zdefiniowane w pliku [res / values / strings]:

  

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

    <string name="app_name">android-domotique</string>

    <!-- Fragmenty i zakładki -->
    <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>

    <!-- Konfiguracja -->
    <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>

Widok wykorzystuje kolory (android:textColor w wierszach 51 i 62) zdefiniowane w pliku [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>
  <!-- aplikacja -->
  <color name="red">#FF0000</color>
  <color name="blue">#0000FF</color>
  <color name="wheat">#FFEFD5</color>
</resources>

Widok wykorzystuje wymiary (android:textSize w wierszu 16), które są zdefiniowane w pliku [res / values / dimens]:

  

<resources>
  <!-- Domyślne marginesy ekranu, zgodnie z wytycznymi projektowymi systemu 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>
  <!-- aplikacja -->
  <dimen name="titre">30dp</dimen>
</resources>

Technika ta nie została zastosowana w przypadku wszystkich wymiarów. Jest to jednak zalecane rozwiązanie. Pozwala ono na zmianę wymiarów w jednym miejscu.


Zadanie: utwórz powyższe elementy.


Uruchom ponownie projekt. Powinieneś uzyskać następujący widok:

Image

5.6.10. Fragment [ConfigFragment]

  

Aby obsłużyć nowy widok [config], kod fragmentu [ConfigFragment] zmienia się w następujący sposób:


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 {

  // elementy interfejsu użytkownika
  @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() {
  }

  // zarządzanie cyklem życia fragmentu -------------------------------------

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

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
  }

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

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
  }

  // metody prywatne --------------------------------------------

  private void initButtons() {
    // przycisk [Exécuter] zastępuje przycisk [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
}
  • wiersze 23–32: elementy interfejsu wizualnego;
  • wiersze 58–60: podczas pierwszej wizyty w fragmencie komunikat o błędzie jest ukryty;
  • wiersze 73–76: za każdym razem, gdy fragment zostanie wyświetlony, przycisk [Annuler] zostanie ukryty (wiersz 82), a przycisk [Rafraîchir] zostanie wyświetlony (wiersze 86–87). W tej aplikacji fragment nie może być wyświetlany, gdy trwa operacja asynchroniczna, a zatem przycisk [Annuler] jest widoczny;

Zadanie: utwórz powyższe elementy.


Uruchom tę nową wersję. Pierwszy widok powinien teraz wyglądać następująco:

Image

5.6.10.1. Przycisk [Rafraîchir]

Na razie obsłużymy kliknięcie przycisku [Rafraîchir] w następujący sposób:


@Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // uruchomimy zadanie – przygotowujemy się do oczekiwania
    beginWaiting(1);
  }

  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // anulujemy zadania asynchroniczne
    cancelRunningTasks();
  }

  protected void beginWaiting(int numberOfRunningTasks) {
    // przygotowujemy oczekiwanie na zadania
    beginRunningTasks(numberOfRunningTasks);
    // przycisk [Annuler] zastępuje przycisk [Rafraîchir]
    btnRafraichir.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
}
  // zarządzanie cyklem życia fragmentu -------------------------------------
...
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // przyciski w stanie początkowym
    initButtons();
  }

  // metody prywatne --------------------------------------------

  private void initButtons() {
    // przycisk [Exécuter] zastępuje przycisk [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
  • wiersze 1–5: metoda wykonywana po kliknięciu przycisku [Rafraîchir];
  • wiersz 4: rozpoczynamy oczekiwanie;
  • wiersz 18: przekazujemy do klasy nadrzędnej liczbę zadań asynchronicznych, które zamierzamy uruchomić. Pojawi się obrazek oczekiwania;
  • wiersze 20–21: oczekiwanie to spowoduje pojawienie się przycisku [Annuler], zniknięcie przycisku [Rafraîchir] oraz pojawienie się obrazka oczekiwania. Nic więcej się nie dzieje. Użytkownik może jednak kliknąć przycisk [Annuler]. Wówczas zostanie wykonana metoda z wierszy 7–14;
  • wiersz 13: zwracamy się do klasy nadrzędnej o anulowanie wszystkich zadań. Klasa to wykona, a następnie wywoła metodę z wierszy 25–29, aby zgłosić, że wszystkie zadania zostały zakończone. Parametr [runningTasksHaveBeenCanceled] przyjmie wartość true, wskazując, że zadania zostały anulowane;
  • wiersze 35–36: przycisk [Annuler] zniknie, natomiast przycisk [Rafraîchir] pojawi się ponownie.

Zadanie: Wprowadź te zmiany, a następnie uruchom projekt. Sprawdź, czy przycisk [Rafraîchir] uruchamia tryb oczekiwania, a przycisk [Annuler] go zatrzymuje. Przejrzyj logi.


5.6.10.2. Sprawdzanie poprawności danych

W poprzedniej wersji nie sprawdzaliśmy poprawności wprowadzonych danych. Aby to zrobić, dodajemy następujący kod w pliku [ConfigFragment]:


// wprowadzone wartości
  private String urlServiceRest;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // sprawdzane są wprowadzone dane
    if (!pageValid()) {
      return;
    }
    // uruchomimy zadanie – przygotowujemy się do oczekiwania
    beginWaiting(1);
  }

  // weryfikacja wprowadzonych danych
  private boolean pageValid() {
    // na początku brak komunikatu o błędzie
    txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    // pobieramy adres IP i port serwera
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    // sprawdzamy ich poprawność
    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) {
      // wyświetlenie komunikatu o błędzie
      txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
      // powrót do UI
      return false;
    }
    // wszystko w porządku
    return true;
  }
  • wiersz 2: wprowadzone dane URL;
  • wiersze 7–9: przed podjęciem jakichkolwiek działań sprawdzamy poprawność wprowadzonych danych;
  • wiersz 19: pobieramy wprowadzony obiekt URL i dodajemy do niego prefiks [http://];
  • wiersz 22: próbuje się utworzyć obiekt URI (Uniform Resource Identifier) na jego podstawie. Jeśli wprowadzony ciąg URL jest niepoprawny pod względem składniowym, zgłaszany jest wyjątek;
  • wiersze 23–27: generowany jest wyjątek, jeśli URI jest poprawny, ale występują również [host==null] i [port==-1]. Jest to możliwy przypadek;
  • wiersz 30: wystąpił wyjątek. Wyświetlany jest komunikat o błędzie;
  • wiersz 32: zwracamy [false], aby wskazać, że strona jest nieprawidłowa;
  • wiersz 35: nie wystąpiły żadne błędy. Zwracamy kod [true], aby wskazać, że strona jest poprawna;

Zadanie: utwórz powyższe elementy.


Przetestuj tę nową wersję i sprawdź, czy nieprawidłowe kody URL są prawidłowo sygnalizowane.

5.6.10.3. Wyświetlanie listy urządzeń Arduino

  

Różne widoki będą musiały wyświetlać listę podłączonych urządzeń Arduino. W tym celu zdefiniujemy różne klasy oraz widok XML:

  • moduł Arduino będzie reprezentowany przez klasę [Arduino] [1];
  • klasa [CheckedArduino] [1] dziedziczy po klasie [Arduino], do której dodano zmienną logiczną określającą, czy Arduino zostało wybrane z listy, czy nie;

Klasa [Arduino] jest tą samą klasą, która jest już używana przez serwer i została przedstawiona w paragrafie 5.4.2. Wygląda ona następująco:


package android.arduinos.entities;

import java.io.Serializable;

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

// metody pobierające i ustawiające
...
}
  • wiersz 7: [id] to identyfikator Arduino;
  • wiersz 8: jego opis;
  • wiersz 9: jego adres MAC;
  • wiersz 10: jego adres IP;
  • wiersz 11: port, na którym oczekuje poleceń;

Ta klasa odpowiada ciągowi znaków jSON otrzymanemu z serwera po wysłaniu zapytania o listę podłączonych modułów Arduino:

Klasa [CheckedArduino] dziedziczy po klasie [Arduino]:


package android.arduinos.entities;

public class CheckedArduino extends Arduino {
    private static final long serialVersionUID = 1L;
    // można wybrać Arduino
    private boolean isChecked;

    // konstruktor
    public CheckedArduino(Arduino arduino, boolean isChecked) {
        // klasa nadrzędna
        super(arduino.getId(), arduino.getDescription(), arduino.getMac(), arduino.getIp(), arduino.getPort());
        // lokalny
        this.isChecked = isChecked;
    }

    // metody pobierające i ustawiające
    public boolean isChecked() {
        return isChecked;
    }

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

}
  • wiersz 3: klasa [CheckedArduino] dziedziczy po klasie [Arduino];
  • wiersz 6: dodajemy do niej zmienną logiczną, która pozwoli nam ustalić, czy na wyświetlanej liście urządzeń Arduino zostało wybrane jakieś urządzenie, czy nie;

W klasie [ConfigFragment] zasymulujemy pobieranie listy podłączonych urządzeń Arduino.

  

  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
..
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // sprawdzamy wprowadzone dane
    if (!pageValid()) {
      return;
    }
    // uruchomimy zadanie – przygotowujemy się do oczekiwania
    beginWaiting(1);
    // czyścimy listę urządzeń Arduino
    clearArduinos();
    // pobieranie listy urządzeń Arduino w tle
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
   ...
  }

  // resetowanie listy urządzeń Arduino
  private void clearArduinos() {
    // tworzymy pustą listę
    List<String> strings = new ArrayList<>();
    // wyświetlanie listy
    listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
  • wiersz 2: ListView, który wyświetla urządzenia Arduino podłączone do serwera;
  • wiersz 5: metoda, która żąda listy podłączonych urządzeń Arduino;
  • wiersz 11: informujemy klasę nadrzędną, że zamierzamy uruchomić zadanie asynchroniczne;
  • wiersz 12: kasujemy aktualnie wyświetlaną listę urządzeń Arduino;
  • wiersz 15: w tle wysyłamy żądanie listy podłączonych modułów Arduino;
  • wiersze 23–28: metoda, która usuwa aktualnie wyświetlaną listę urządzeń Arduino;

Metoda [getArduinosInBackground] wygląda następująco:


  private void getArduinosInBackground() {
    // tworzy się fikcyjną listę urządzeń 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));
    }
    // symulujemy odpowiedź serwera
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // anuluje się oczekiwanie
    cancelWaitingTasks();
    // zmieniamy przyciski
    initButtons();
    // przetwarzamy odpowiedź
    consumeArduinosResponse(response);
}
  • wiersze 3–6: tworzy się listę 20 urządzeń Arduino;
  • wiersze 8–9: tworzymy odpowiedź typu [Response<List<Arduino>>] (punkt 5.4.2), która będzie zawierała utworzoną listę modułów Arduino;
  • wiersz 11: anulujemy oczekiwanie;
  • wiersz 13: przywracamy przyciski do stanu początkowego;
  • wiersz 15: odczytujemy odpowiedź;

Metoda [consumeArduinosResponse] wygląda następująco:


  // wyświetlanie odpowiedzi
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // błąd?
    if (response.getStatus() != 0) {
      // wyświetlanie
      showAlert(response.getMessages());
      // powrót do interfejsu użytkownika
      return;
    }
    // tworzymy listę [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // wyświetla się
    showArduinos(checkedArduinos);
}
  • wiersze 4–11: sprawdzamy kod błędu w odpowiedzi wysłanej przez serwer:
  • wiersz 4: jeśli kod błędu jest różny od zera;
  • wiersz 6: wyświetla się komunikaty zapisane przez serwer w polu [messages] odpowiedzi;
  • wiersz 8: powrót do interfejsu użytkownika;
  • wiersze 11–16: jeśli nie wystąpiły żadne błędy, wyświetla się otrzymana lista urządzeń Arduino po przekształceniu jej na typ List<CheckedArduino>;

Metoda [showArduinos] wygląda następująco:


  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // tworzymy listę ciągów znaków na podstawie listy urządzeń Arduino
    List<String> strings = new ArrayList<>();
    for (CheckedArduino checkedArduino : checkedArduinos) {
      strings.add(checkedArduino.toString());
    }
    // wyświetla się
    listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}

Zadanie: wprowadź powyższe zmiany i uruchom swój projekt.


Po kliknięciu przycisku [Rafraîchir] powinien pojawić się następujący widok:

Image

Wpis w polu [1] nie jest wykorzystywany. Można więc wpisać dowolną wartość, o ile jest zgodna z oczekiwanym formatem.

5.6.10.4. Szablon do wyświetlania Arduino

Obecnie podłączone urządzenia Arduino są wyświetlane w widoku [Config] w następujący sposób:

Image

Chcemy teraz wyświetlać je w następujący sposób:

Image

  • w widoku [1] – pole wyboru, które pozwoli na wybranie urządzenia Arduino. Pole to będzie ukryte, gdy chcemy wyświetlić listę urządzeń Arduino, których nie można wybrać;
  • w [2] – identyfikator Arduino;
  • w [3] – jego opis;

Poniższe informacje nawiązują do koncepcji omówionych w projektach [exemple-19] i [exemple-19B] z paragrafu 1.20. W razie potrzeby należy do nich wrócić.

Najpierw tworzymy widok, który wyświetli element z listy urządzeń Arduino:

 

Kod powyższego widoku [listarduinos_item] wygląda następująco:


<?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>
  • wiersze 9–15: pole wyboru;
  • wiersze 17–23: tekst [Id : ];
  • wiersze 25–33: w tym miejscu zostanie wpisany identyfikator Arduino;
  • wiersze 35–43: tekst [Description : ];
  • wiersze 45–53: w tym miejscu zostanie wpisany opis Arduino;

Ten widok wykorzystuje teksty (wiersze 23, 32, 43) zdefiniowane w pliku [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>

Widok wykorzystuje również kolor (wiersze 33, 53) zdefiniowany w pliku [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>

Menedżer wyświetlania elementu z listy urządzeń Arduino

  

Klasa [ListArduinosAdapter] jest klasą wywoływaną przez klasę [ListView] w celu wyświetlenia poszczególnych elementów listy urządzeń Arduino. Jej kod wygląda następująco:


package istia.st.android.vues;

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

public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {

    // tabela Arduino
    private List<CheckedArduino> arduinos;
    // kontekst wykonania
    private Context context;
    // identyfikator układu wyświetlania wiersza na liście Arduino
    private int layoutResourceId;
    // czy wiersz zawiera pole wyboru
    private Boolean selectable;

    // konstruktor
    public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
        // element nadrzędny
        super(context, layoutResourceId, arduinos);
        // zapisywanie informacji
        this.arduinos = arduinos;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.selectable = selectable;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • wiersz 18: konstruktor klasy przyjmuje cztery parametry: aktualnie wykonywaną aktywność, identyfikator widoku, który ma być wyświetlany dla każdego elementu źródła danych, źródło danych zasilające listę oraz wartość logiczną wskazującą, czy pole wyboru powiązane z każdym Arduino ma być wyświetlane, czy nie;
  • wiersze 8–15: te cztery informacje są zapisywane lokalnie;

Wiersz 29: metoda [getView] odpowiada za wygenerowanie widoku nr [position] w [ListView] oraz za obsługę związanych z nim zdarzeń. Jej kod wygląda następująco:


@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // bieżące Arduino
        final CheckedArduino arduino = arduinos.get(position);
        // tworzy się bieżący wiersz
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // pobieramy odniesienia z [TextView]
        TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
        TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
        // wypełnianie wiersza
        txtArduinoId.setText(arduino.getId());
        txtArduinoDesc.setText(arduino.getDescription());
        // CheckBox nie zawsze jest widoczne
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // przypisuje się mu wartość
            ck.setChecked(arduino.isChecked());
            // obsługujemy kliknięcie
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
        }
        // wyświetla się wiersz
        return row;
    }
  • wiersz 2: pierwszy parametr to pozycja w pliku [ListView] wiersza, który ma zostać utworzony. Jest to również pozycja na lokalnie zapisanej liście urządzeń Arduino;
  • wiersz 4: pobierany jest identyfikator Arduino, które zostanie powiązane z tworzonym wierszem;
  • wiersz 6: bieżący wiersz jest tworzony na podstawie widoku [listarduinos_item.xml];
  • wiersze 8–9: pobierane są odwołania do obu widoków [TextView];
  • wiersze 11–12: obie wartości [TextView] otrzymują swoje wartości;
  • wiersz 14: pobierane jest odwołanie do pola wyboru;
  • wiersz 15: pole wyboru jest wyświetlane lub ukrywane, w zależności od wartości [selectable] przekazanej początkowo do konstruktora;
  • wiersz 16: jeśli pole wyboru jest obecne;
  • wiersz 18: przypisuje się mu wartość [isChecked] z bieżącego Arduino;
  • wiersze 20–26: obsługa kliknięcia na polu wyboru;
  • wiersz 23: wartość pola wyboru jest zapisywana w bieżącym Arduino;

Obsługa listy urządzeń Arduino

Wyświetlanie listy urządzeń Arduino jest obecnie obsługiwane przez dwie metody klasy [ConfigFragment]:

  • [clearArduinos]: która wyświetla pustą listę;
  • [showArduinos]: wyświetla listę zwróconą przez serwer;

Te dwie metody zmieniają się w następujący sposób:


  // wyzerowujemy listę urządzeń Arduino
  private void clearArduinos() {
    // wyświetla się pusta lista
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }

  // wyświetlanie listy urządzeń Arduino
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // wyświetlanie urządzeń Arduino
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
}

Zadanie: Wprowadź te zmiany i przetestuj nową aplikację.


Image

5.6.10.5. Sesja

Sesja to miejsce, w którym umieszczamy informacje współdzielone przez fragmenty i aktywność. Wszystkie fragmenty muszą wyświetlać listę podłączonych urządzeń Arduino. Dlatego pierwsza wersja sesji będzie wyglądać następująco:


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 {
  // dane do współdzielenia między samymi fragmentami oraz między fragmentami a aktywnością
  // elementy, których nie można zserializować w jSON, muszą posiadać adnotację @JsonIgnore
  // nie zapomnij o metodach getter i setter niezbędnych do serializacji / deserializacji w formacie jSON

  // lista urządzeń Arduino
  private List<CheckedArduino> checkedArduinos = new ArrayList<>();

  // metody pobierające i ustawiające
...
}

Zadanie: Utwórz powyższą klasę [Session].


Utworzenie tej sesji wymaga wprowadzenia następujących zmian w już napisanym kodzie:


  // wyświetlanie odpowiedzi
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // błąd?
    if (response.getStatus() != 0) {
      // wyświetlanie
      showAlert(response.getMessages());
      // anulowanie
      doAnnuler();
      // powrót do interfejsu użytkownika
      return;
    }
    // tworzymy listę [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // przenosimy ją do sesji
    session.setCheckedArduinos(checkedArduinos);
    // wyświetla się je
    showArduinos(checkedArduinos);
    // anuluje się oczekiwanie
    cancelWaitingTasks();
}
  • wiersz 18: lista modułów Arduino utworzona w poprzednich wierszach jest umieszczana w sesji;

5.6.10.6. Zarządzanie stanem fragmentu

Podczas obrotu urządzenia elementy wizualne widoku są (domyślnie) odtwarzane w stanie, w jakim znajdowały się podczas projektowania widoku:

  • [ListView] zawiera elementy, które umieścił w nim projektant;
  • komunikat o błędzie jest w stanie widocznym lub niewidocznym, w jakim umieścił go projektant;

Stany elementów wizualnych z etapu projektowania mogą, ale nie muszą być odpowiednie podczas przywracania fragmentu. Jak wygląda sytuacja w tym przypadku?

  • [ListView] powinien wyświetlać listę podłączonych urządzeń Arduino. Nie można zatem wykorzystać wartości [ListView] z etapu projektowania;
  • [TextView] z komunikatu o błędzie musi zostać przywrócony w stanie widocznym lub niewidocznym, jaki miał w momencie zapisania. Jego wartość z projektu może nie pasować do żadnego z tych dwóch przypadków;

Musimy zatem zapisać stan tych dwóch komponentów podczas zapisywania stanu fragmentu:

  • lista podłączonych urządzeń Arduino;
  • widoczność (wyświetlany / ukryty) komunikatu o błędzie podczas wprowadzania kodu URL serwisu internetowego / jSON;

Ponieważ lista podłączonych urządzeń Arduino jest dostępna w sesji, zostanie ona automatycznie zapisana. Widoczność komunikatu o błędzie zostanie zapisana w następującej klasie [ConfigFragmentState]:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class ConfigFragmentState extends CoreState {

  // widoczność komunikatu o błędzie
  private boolean txtMsgErreurUrlServiceRestVisible;

  // metody pobierające i ustawiające
...
}

Zadanie: utwórz poprzednią klasę [ConfigFragmentState].


Aby poprawnie odtworzyć stany fragmentów, należy zmodyfikować ich metody [getNumView] i [saveFragment]. Na przykład metoda fragmentu [BlinkFragment] ma obecnie następującą postać:


  @Override
  public CoreState saveFragment() {
    // należy zapisać fragment
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // jeślinie ma nic do zapisania, należy wykonać [return new CoreState();] i usunąć klasę [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // należy zwrócić numer fragmentu do tabeli fragmentów zarządzanych przez aktywność (patrz MainActivity)
    return 0;
}

Jeśli nie podejmie się żadnych działań, stan wygenerowany w wierszu 6 zostanie zapisany w elemencie 0 (wiersz 13) tablicy CoreState[] coreStates klasy [AbstractSession] (wiersz 5 poniżej):


public class AbstractSession implements ISession {
  ...

  // stan widoków
  private CoreState[] coreStates = new CoreState[0];
...

Powinien on jednak zostać zapisany w elemencie odpowiadającym numerowi fragmentu [BlinkFragment] w tablicy fragmentów zdefiniowanych w klasie [MainActivity] (wiersz 9 poniżej):


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


Numery fragmentów zostały zdefiniowane w interfejsie [IMainActivity]:


public interface IMainActivity extends IDao {

  ...

  // numery widoków
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}

Ostatecznie fragment o numerze [BlinkFragment] będzie obsługiwany poprawnie, jeśli wpiszemy:


  @Override
  public CoreState saveFragment() {
    // należy zapisać fragment
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // jeślinie ma nic do zapisania, należy wykonać [return new CoreState();] i usunąć klasę [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // należy zwrócić numer fragmentu do tabeli fragmentów zarządzanych przez aktywność (patrz MainActivity)
    return IMainActivity.VUE_BLINK;
}
  • wiersz 14: zwracany jest numer fragmentu [BlinkFragment] w tablicy fragmentów obsługiwanych przez aktywność;

Ponadto klasa [CoreState], będąca klasą nadrzędną dla stanów fragmentów, ma obecnie następujący wygląd (patrz paragraf 5.6.7.2):


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // fragment odwiedzony lub nie
  protected boolean hasBeenVisited = false;
  // stan ewentualnego menu fragmentu
  protected MenuItemState[] menuOptionsState;

  // metody pobierające i ustawiające
....
}
  • wiersze 12–16: klasa [DummyFragmentState] nie figuruje na liście klas potomnych klasy [CoreState]. Tymczasem metoda [saveFragment] klasy [BlinkFragment] zwraca obecnie typ [ DummyFragmentState]. Jeśli pozostawimy tę sytuację bez zmian, serializacja/deserializacja sesji zakończy się niepowodzeniem, a sesja nie zostanie przywrócona, co doprowadzi do awarii aplikacji;

Metoda [saveFragment] z fragmentu [BlinkFragment] musi zostać przepisana w następujący sposób:


  @Override
  public CoreState saveFragment() {
    // fragment należy zapisać
    BlinkFragmentState state=new BlinkFragmentState();
    // ...
    return state;
    // jeślinie ma nic do zapisania, należy wykonać [return new CoreState();] i usunąć klasę [DummyFragmentState]
}

Zadanie: w każdym z fragmentów zmodyfikuj metodę [getNumView] tak, aby zwracała numer fragmentu, oraz metodę [saveFragment] tak, aby zwracała instancję klasy stanu fragmentu (jak powyżej).


5.6.10.7. Zarządzanie cyklem życia fragmentu

W tym miejscu skupiamy się na cyklu życia fragmentu [ConfigFragment], a w szczególności na czterech metodach:

  • [saveFragment]: musi zapisać stan fragmentu, aby można go było przywrócić w późniejszym czasie;
  • [initFragment]: która musi zainicjować niektóre pola fragmentu, jeśli zajdzie taka potrzeba. Metoda ta jest wywoływana podczas uruchamiania aplikacji oraz za każdym razem, gdy następuje obrót urządzenia. Dokładniej rzecz biorąc, jest wywoływana, gdy fragment staje się widoczny po jednym z dwóch poprzednich zdarzeń;
  • [initView]: która w razie potrzeby musi zainicjować niektóre komponenty widoku. Metoda ta jest wywoływana za każdym razem, gdy została wywołana metoda [initFragment] oraz gdy widok musi zostać odświeżony, ponieważ fragment w danym momencie znalazł się poza obszarem sąsiedztwa wyświetlanego fragmentu. Podobnie jak poprzednio, jest ona wywoływana, gdy fragment staje się widoczny po wystąpieniu jednego z tych zdarzeń;
  • [updateOnRestore]: która jest wykonywana po dwóch poprzednich metodach w przypadku obrotu urządzenia, a także w przypadku nawigacji. Jej zadaniem jest przywrócenie poprzedniego stanu fragmentu;

Metody te będą następujące:


// adapter listy urządzeń Arduino
  private ListArduinosAdapter adapterListArduinos;

...
  // zarządzanie cyklem życia fragmentu -------------------------------------

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

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

  }

  @Override
  protected void initView(CoreState previousState) {
    // połączenie listview / adapter
    listArduinos.setAdapter(adapterListArduinos);
    // Pierwsza wizyta?
    if (previousState == null) {
      // ListView pusty – utworzony przez [initFragment]
      // ukryty komunikat o błędzie
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    } else {
      // przywracamy widoczność komunikatu o błędzie
      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() {
    // przyciski
    initButtons();
}
  • wiersz 2: adapter ListView dla Arduino. Jest to zmienna globalna, ponieważ jest używana w różnych metodach;
  • wiersze 7–12: metoda [saveFragment] zapisuje w typie [ConfigFragmentState] widoczność TextView txtMsgErreurUrlServiceRestVisible (wiersz 10);
  • wiersze 14–19: metoda [initFragment] inicjuje adapter z wiersza 2, wykorzystując listę urządzeń Arduino obecnych w sesji (wiersz 17). Przypomnijmy, że rolą [initFragment] jest zainicjowanie pól fragmentu. W tym przypadku inicjalizacja ta musi zostać przeprowadzona w każdym przypadku, niezależnie od tego, czy jest to pierwsza wizyta (previousState==null), czy nie;
  • wiersz 17: widać, że adapter jest powiązany ze źródłem danych [session.getCheckedArduinos]. Nie może ono przyjmować wartości null. Z tego powodu pole [session.checkedArduinos] jest inicjowane pustą listą w sesji:

  // lista urządzeń Arduino
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
  • wiersze 21–35: metoda [initView] służy do inicjalizacji niektórych elementów interfejsu wizualnego, w szczególności tych, których wartość nie jest zachowywana podczas obracania urządzenia;
  • wiersz 24: metoda ListView dla Arduino jest powiązana z adapterem z wiersza 2;
  • wiersze 28–32: rozróżnia się pierwszą wizytę od pozostałych;
  • wiersz 29: podczas pierwszej wizyty należy wyświetlić pusty [ListView]. Tak właśnie jest, ponieważ podczas pierwszej wizyty adapter [ListView] został powiązany z pustą listą (wiersz 17);
  • wiersz 31: komunikat o błędzie jest ukryty;
  • wiersze 32–36: sytuacja, w której nie jest to pierwsza wizyta;
  • [ListView] jest już w prawidłowym stanie od wiersza 24. Nie ma nic więcej do zrobienia;
  • wiersze 34–35: przywracamy komunikat o błędzie do stanu, w jakim znajdował się podczas ostatniego zapisania fragmentu;
  • wiersze 31–36: metoda [updateOnRestore] musi przywrócić fragment do stanu początkowego. Do metody [updateOnRestore] można dotrzeć na dwa sposoby:
    • albo z powodu obrotu urządzenia. W takim przypadku wszystkie niezbędne inicjalizacje zostały już przeprowadzone w metodzie [initView];
    • albo z powodu przejścia z jednej zakładki do zakładki [Config]. Jeśli fragment [Config] wyszedł z sąsiedztwa wyświetlanych fragmentów od momentu opuszczenia go, to metoda [initView] została już wykonana, a fragment znajduje się już w pożądanym stanie. Jeśli fragment [Config] nie wyszedł z sąsiedztwa wyświetlanych fragmentów od momentu opuszczenia go, to jego elementy wizualne nie zmieniły stanu i nie ma nic do zrobienia;

Widać, że metoda [updateOnRestore] nie ma nic do zrobienia. Czasami tak jest, a czasami nie. Różnica wynika z metody [updateOnSubmit]: jeśli ta metoda wykonuje czynność, która sprawia, że niektóre inicjalizacje przeprowadzone w metodzie [initView] stają się zbędne, wówczas inicjalizacje te powinny zostać przeprowadzone w metodzie [updateOnRestore]. Weźmy na przykład przycisk opcji z trzema wartościami: V1, V2, V3. Być może w przypadku nawigacji powiązanej z akcją [SUBMIT] zaznaczony przycisk opcji powinien zawsze mieć wartość V1. W takim przypadku przywracanie wartości przycisku opcji w metodzie [initView] jest zbędne, ponieważ w przypadku akcji [SUBMIT] wartość ta zostanie zastąpiona wartością podaną przez metodę [updateOnSubmit]. W związku z tym lepiej jest przenieść to przywracanie do metody [updateOnRestore], aby uniknąć wykonywania czasami zbędnej operacji.

  • wiersze 48–52: metoda [notifyEndOfUpdates] jest wykonywana po wszystkich poprzednich;
  • wiersz 51: przyciski są przywracane do stanu początkowego: przycisk [Rafraîchir] wyświetlony, przycisk [Annuler] ukryty:

Zadanie: dodaj powyższy kod do metody [ConfigFragment], a następnie uruchom aplikację. Sprawdź, czy po obróceniu urządzenia zakładka [Config] zachowuje swój stan (komunikat o błędzie, lista urządzeń Arduino). Sprawdź, czy tak samo dzieje się podczas zwykłego przechodzenia między kartami: karta [config] → karta [Commands] → karta [Config]. W tym ostatnim przypadku, jeśli w zakładce [IMainActivity] zachowano sąsiedztwo fragmentów wynoszące 1, to widok fragmentu [ConfigFragment] zostanie zniszczony podczas przejścia do zakładki [Commands], a następnie odtworzony po powrocie do zakładki [Config]. Podczas testów należy sprawdzić logi.


5.6.10.8. Ulepszenie kodu

Kod fragmentu [ConfigFragment] można ulepszyć. Na przykład napisaliśmy:


// adapter listy urządzeń Arduino
  private ListArduinosAdapter adapterListArduinos;

...

  // wyświetlanie listy urządzeń Arduino
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // wyświetlanie modułów Arduino
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
  }

  // wyczyść listę modułów Arduino
  private void clearArduinos() {
    // wyświetlanie pustej listy
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
  • widać, że w wierszach 9 i 16 używamy zmiennej lokalnej niezwiązanej z polem z wiersza 2, podczas gdy w rzeczywistości chcemy operować na tej samej jednostce;

Modyfikujemy kod w następujący sposób:


  // adapter listy urządzeń Arduino
  private ListArduinosAdapter adapterListArduinos;

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

  private void getArduinosInBackground() {
 ...
    // przetwarzanie listy
    consumeArduinosResponse(response);
  }

  // wyświetlanie odpowiedzi
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // błąd?
    if (response.getStatus() != 0) {
      // wyświetlanie
      showAlert(response.getMessages());
      // anulowanie
      doAnnuler();
      // powrót do interfejsu użytkownika
      return;
    }
    // tworzymy listę [CheckedArduino]
    List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
    checkedArduinos.clear();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // wyświetla się
    adapterListArduinos.notifyDataSetChanged();
    // anulowanie oczekiwania
    cancelWaitingTasks();
}
  
  @Override
  protected void initFragment(CoreState previousState) {
    // adapter listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);

  }

  @Override
  protected void initView(CoreState previousState) {
    // powiązanie listview / adapter
    listArduinos.setAdapter(adapterListArduinos);
    ...
}
  • gdy metoda z wiersza 5 zostanie wykonana, cykl życia fragmentu zostanie zakończony. A zatem:
    • adapter z linii 2 został powiązany ze swoim źródłem danych (linia 41);
    • [ListView] podłączonych urządzeń Arduino zostało połączone z tym adapterem (wiersz 48);

Gdy chcemy zmienić wyświetlanie [ListView], należy wykonać dwie czynności:

  • zmienić zawartość źródła danych [session.checkedArduinos];
  • zgłosić tę zmianę do adaptera za pomocą instrukcji [adapterListArduinos.notifyDataSetChanged()];

Chodzi tu właśnie o zmianę zawartości źródła danych, a nie samego źródła danych. Jeśli zmienimy samo źródło danych, operacja [adapterListArduinos.notifyDataSetChanged()] będzie nadal wyświetlać stare źródło danych. W takim przypadku należałoby powiązać adapter z nowym źródłem danych.

Kod wygląda następująco:

  • wiersz 27: pobieramy źródło danych;
  • wiersz 28: opróżniamy je. Z tego powodu usunęliśmy metodę [clearArduinos];
  • wiersze 29–31: do tej pustej listy dodajemy nowe elementy;
  • wiersz 33: nakazuje się adapterowi odświeżenie. Spowoduje to odświeżenie wyświetlania powiązanego elementu [ListView];

Zadanie: wprowadź te zmiany i sprawdź, czy aplikacja nadal działa.


5.6.11. Komunikacja między widokami

Aby sprawdzić komunikację między widokami, sprawimy, że wszystkie pozostałe widoki będą wyświetlać listę urządzeń Arduino uzyskaną przez widok [Config]. Zacznijmy od widoku [blink.xml]. Chociaż wcześniej nie wyświetlał on niczego, teraz będzie wyświetlał listę podłączonych urządzeń Arduino:

Image

 

Kod XML dla widoku [blink.xml] będzie wyglądał następująco:


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

Kod ten został przejęty bezpośrednio z widoku [config.xml]. Zmieniono jedynie górny margines w wierszu 19.


Zadanie: skopiuj ten kod do widoków [commands.xml, pinread.xml, pinwrite.xml].


Kod fragmentu [BlinkFragment] powiązanego z widokiem [blink.xml] również ulega zmianie:

  

  // elementy wizualne
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;

  // adapter listy urządzeń Arduino
  private ListArduinosAdapter adapterListArduinos;
...

  // metody narzucone przez klasę nadrzędną -------------------------------------------------------

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

  }

  @Override
  protected void initView(CoreState previousState) {
    // powiązanie listview / adapter
    listArduinos.setAdapter(adapterListArduinos);
  }
...
  • wiersze 2–3: komponent [ListView] podłączonych urządzeń Arduino;
  • wiersz 6: adapter tego komponentu [ListView];
  • wiersze 12–23: kod metod [initFragment] i [initView] jest taki sam, jak ten używany już dla fragmentu [ConfigFragment];
  • wiersz 15: gdy fragment wymaga zresetowania, resetuje się adapter z wiersza 2, powiązując go z listą urządzeń Arduino zapisaną w sesji. Ostatni parametr [true] konstruktoru [ListArduinosAdapter] oznacza, że obok każdego Arduino ma być wyświetlane pole wyboru;
  • wiersz 22: gdy widok fragmentu musi zostać zresetowany, przypisujemy [ListView] podłączonych urządzeń Arduino do adaptera z wiersza 6;

Zadanie: Skopiuj ten kod do pozostałych fragmentów o nazwie [CommandsFragment, PinReadFragment, PinWriteFragment]. Uruchom aplikację i sprawdź, czy każda zakładka wyświetla listę podłączonych modułów Arduino. Zwróć również uwagę, że jeśli zaznaczysz moduły Arduino w jednej zakładce, a następnie przejdziesz do innej, pozostaną one zaznaczone również w tej drugiej.


Uwaga: Wyjaśnienie dotyczące zachowania zaznaczenia modułów Arduino jest następujące. Klasa [ListArduinosAdapter] została przedstawiona w paragrafie 5.6.10.4. Kod związany z polem wyboru jest następujący:


        // aktualne Arduino
        final CheckedArduino arduino = arduinos.get(position);
...
        // CheckBox nie zawsze jest widoczny
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // przypisuje się mu wartość
            ck.setChecked(arduino.isChecked());
            // obsługujemy kliknięcie
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
}
  • wiersze 11–15: jeśli w zakładce X zaznaczy się pole wyboru, właściwość [checked] urządzenia Arduino z wiersza 2 zostanie zmieniona na true (wiersz 14);
  • po przejściu do zakładki Y wyświetlana jest wartość [ListView] dla modułów Arduino z tej zakładki. W wierszu 9 widać, że jeśli właściwość [checked] Arduino z wiersza 2 zostanie zmieniona na true, to pole wyboru [ck] w wierszu 5 zostanie zaznaczone;

5.6.12. Warstwa [DAO]

Uwaga: w tej części należy zapoznać się z implementacją warstwy [DAO] w projekcie [exemple-16B] (patrz punkt 2.8.3).

Jak dotąd ręcznie generowaliśmy listę podłączonych urządzeń Arduino. Teraz będziemy ją pobierać z serwera WWW / jSON. W tym celu stworzymy warstwę [DAO]:

  

5.6.12.1. Interfejs IDao

Interfejs [IDao] warstwy [DAO] będzie wyglądał następująco:


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 {
  // adres URL serwisu internetowego
  void setUrlServiceWebJson(String url);

  // użytkownik
  void setUser(String user, String mdp);

  // limit czasu klienta
  void setTimeout(int timeout);

  // podstawowe uwierzytelnianie
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // tryb debugowania
  void setDebugMode(boolean isDebugEnabled);

  // czas oczekiwania klienta przed wysłaniem żądania w milisekundach
  void setDelay(int delay);

  // specyficzne ----------------------------------------
  // lista urządzeń Arduino
  Observable<Response<List<Arduino>>> getArduinos();
}
  • wiersze 11–26: te wiersze występują już w interfejsie [IDao] projektu wzorcowego [client-android-skel];
  • wiersz 30: metoda [getArduinos] pozwala uzyskać listę podłączonych urządzeń Arduino w postaci obserwowalnej typu Observable<[Response<List<Arduino>>>];

Przypominamy, że [Response<T>] jest typem wszystkich odpowiedzi wysyłanych przez serwer w postaci ciągu znaków jSON:


package client.android.dao.entities;

import java.util.List;

public class Response<T> {

    // ----------------- właściwości
    // status operacji
    private int status;
    // ewentualne komunikaty o błędach
    private List<String> messages;
    // treść odpowiedzi
    private T body;

    // konstruktory
    public Response() {

    }

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

    // metody pobierające i ustawiające
...
}

5.6.12.2. Interfejs [WebClient]

  

Interfejs [WebClient] to interfejs, którego implementację zapewnia biblioteka AA. Interfejs ten będzie wyglądał następująco:


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

  // szczegółowe --------------------------------------
  // lista urządzeń Arduino
  @Get("/arduinos")
  Response<List<Arduino>> getArduinos();
}
  • wiersze 15–19: wiersze te są domyślnie obecne w interfejsie [WebClient] projektu wzorcowego [client-android-skel];
  • wiersz 23: interfejs URL serwera, który umożliwia uzyskanie listy urządzeń Arduino za pomocą operacji GET. Przypominamy, że ten URL jest mierzony względem URL, który jest elementem głównym [RestClientRootUrl] z wiersza 16;
  • wiersz 24: serwer zwraca ciąg jSON typu [Response<List<Arduino>>]. Ten ciąg znaków jSON jest automatycznie deserializowany do typu [Response<List<Arduino>>] za pomocą konwertera jSON [MappingJackson2HttpMessageConverter] z wiersza 15;

5.6.12.3. Klasa [Dao]

Klasa [Dao] implementuje interfejs [IDao] w następujący sposób:


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 {

  // klient serwisu internetowego
  @RestService
  protected WebClient webClient;
  // bezpieczeństwo
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // fabryka RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // dziennik
    Log.d(className, "afterInject");
    // tworzy się restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // ustala się konwerter jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // ustala się restTemplate klienta internetowego
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // ustawiamy URL serwisu internetowego
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // rejestruje się użytkownika w intercepterze
    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));
    }
    // konfiguracja fabryczna
    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));
    }
    // przechwytywacz uwierzytelniania?
    if (isBasicAuthentificationNeeded) {
      // dodajemy interceptor uwierzytelniający
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

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

  // specyficzna implementacja IDao -----------------------------------------------

  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    // działanie klienta internetowego
    return getResponse(new IRequest<Response<List<Arduino>>>() {
      @Override
      public Response<List<Arduino>> getResponse() {
        return webClient.getArduinos();
      }
    });
  }
}
  • wiersze 19–87: wiersze te stanowią podstawę klasy [Dao] w projekcie [client-android-skel];
  • wiersze 91–100: implementacja metody [getArduinos];
  • wiersz 94: wywoływana jest metoda [getResponse] klasy nadrzędnej. Jedynym parametrem tej metody jest instancja interfejsu [IRequest<T>];
  • wiersze 95–99: jedyną metodą interfejsu [IRequest<T>] jest metoda [T getResponse()];
  • wiersz 94: typ T klasy [IRequest<T>] musi być typem T wyniku Observable<T> metody z wiersza 92, a więc w tym przypadku typem [Response<List<Arduino>>];
  • wiersz 97: metoda [IRequest.getResponse()] przekazuje zadanie metodzie [webClient.getArduinos()], którą już omówiliśmy. [webClient], zdefiniowany w wierszu 24, jest instancjonowany przez bibliotekę AA i stanowi instancję interfejsu [WebClient], który już omówiliśmy;

5.6.13. Aktywność [MainActivity]

  

W punkcie 5.6.8 omówiliśmy już aktywność [MainActivity]. Rozszerza ona klasę [AbstractActivity] i w związku z tym implementuje interfejs [IMainActivity], który z kolei rozszerza interfejs [IDao]. Za każdym razem, gdy dodaje się metodę do interfejsu [IDao], należy ją zaimplementować w klasie [MainActivity]. Metoda [IDao.getArduinos] dodana do interfejsu [IDao] zostanie zaimplementowana w następujący sposób w klasie [MainActivity]:


...
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

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

...

  // implementacja IDao -----------------------------------------
  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    return dao.getArduinos();
  }
}
  • wiersze 15–18: metoda [getArduinos] jest zaimplementowana poprzez delegowanie zadania do klasy [Dao], którą właśnie przedstawiono i do której odwołuje się wiersz 8;

5.6.14. Ponowne przyjrzenie się fragmentowi [ConfigFragment]

W klasie [ConfigFragment] kod wykonywany po kliknięciu przycisku [Rafraîchir] wygląda na razie następująco:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    ...
    // w tle pobierana jest lista urządzeń Arduino
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
    // tworzy się fikcyjną listę urządzeń 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));
    }
    // symulacja odpowiedzi serwera
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // odbieramy ją
    consumeArduinosResponse(response);
  }

  // wyświetlanie odpowiedzi
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    ...
}

Musimy przepisać wiersze 10–16, które generowały na stałe odpowiedź typu [Response<List<Arduino>>]. Teraz musimy zażądać tej listy od warstwy [DAO] za pośrednictwem aktywności. Kod wygląda następująco:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // sprawdzamy wprowadzone dane
    if (!pageValid()) {
      return;
    }
    // zapisywanie wprowadzonych danych
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // przygotowuje się do oczekiwania
    beginWaiting(1);
    // wykonuje się zadanie asynchroniczne
    executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {

      @Override
      public void call(Response<List<Arduino>> response) {
        // odbierana jest odpowiedź
        consumeArduinosResponse(response);
      }
    });
}
  • wiersz 8: URL – katalog główny serwisu internetowego / jSON wprowadzony przez użytkownika – jest przekazywany do warstwy [DAO] za pośrednictwem aktywności. Będzie to URL – korzeń interfejsu [WebClient] (patrz paragraf 5.6.12.2);
  • wiersz 10: powiadamiamy klasę nadrzędną, że zamierzamy uruchomić zadanie asynchroniczne;
  • wiersze 12–19: uruchomienie zadania asynchronicznego, które zwróci listę urządzeń Arduino podłączonych do serwera;
  • wiersz 12: wywołanie metody [executeInBackground] klasy nadrzędnej. Metoda ta oczekuje dwóch parametrów:
    • wiersz 12: proces, który ma być obserwowany. Proces ten jest tutaj dostarczany przez metodę [mainActivity.getArduinos()];
    • wiersze 12–19: instancja interfejsu [Action1<T>], gdzie typ T jest typem dostarczonym przez proces, w tym przypadku typem [Response<List<Arduino>>];
  • wiersze 14–18: metoda wywoływana, gdy zadanie asynchroniczne zwraca wynik typu [Response<List<Arduino>>];
  • wiersz 17: przekazujemy otrzymaną odpowiedź do już napisanej metody [consumeArduinosResponse];

Zadanie: Uruchom serwer zgodnie z instrukcją zawartą w punkcie 5.4. Podłącz jedno lub więcej urządzeń Arduino do serwera PC, na którym uruchomiono serwer. Następnie uruchom klienta na Androida i sprawdź, czy udaje się uzyskać listę podłączonych urządzeń Arduino. Przejrzyj logi.


Image

  • wpisz adres URL podany w [1]. Jest to jeden z adresów IP Twojego serwera;
  • kliknij przycisk [2];
  • powinna pojawić się lista podłączonych urządzeń Arduino w [3];

Sprawdź, czy ta lista pojawia się również w pozostałych zakładkach.

5.7. Zadanie do wykonania


Postępując tak samo, jak w przypadku widoku [Config], utwórz, a następnie przetestuj kolejno cztery pozostałe widoki aplikacji: [Blink], [PinRead], [PinWrite] oraz [Commands].


Widoki do wykonania zostały przedstawione w punkcie 5.5.

Dla każdego widoku należy:

  • narysować widok XML (patrz punkt 5.6.9);
  • skonstruować powiązany fragment (patrz punkt 5.6.10);
  • dodać metodę do interfejsu [WebClient] (patrz punkt 5.6.12.2);
  • dodać metodę do interfejsu [IDao] (patrz punkt 5.6.12.2);
  • dodać metodę do klasy [Dao] (patrz punkt 5.6.12.3);
  • dodać metodę do aktywności [MainActivity] (patrz punkt 5.6.13);
  • napisać procedury obsługi zdarzeń fragmentu (patrz punkt 5.6.14);
  • przetestować i przeanalizować logi;

Uwaga 1: przykładem do naśladowania jest projekt [Exemple-16B] z kursu (patrz punkt 2.8.3).

Uwaga 2: URL, do których należy kierować zapytania, oraz typy ich odpowiedzi zostały przedstawione w punkcie 5.4.2.

Uwaga 3:

Klasa [CommandsFragment] wysyła listę zawierającą pojedyncze polecenie do wykonania przez jedno lub więcej urządzeń Arduino. Polecenie to zostanie zamknięte w następującej klasie [ArduinoCommand]:


package android.arduinos.dao;

import java.util.Map;

public class ArduinoCommand {

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

  // konstruktory
  public ArduinoCommand() {

  }

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

  // metody pobierające i ustawiające
...
}

W interfejsie [WebClient] metoda wykonania tej listy zawierającej jedno polecenie będzie następująca:


  // wysyłanie poleceń JSON
  @Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
  • wiersz 2: żądana jest klasa URL wraz z poleceniami HTTP i POST;
  • wiersz 3: wartość wysyłana musi posiadać adnotację [@Body];

Uwaga 4: zaleca się wykonanie tej pracy w następujący sposób:

  • przechodzić do kolejnego widoku dopiero po utworzeniu i przetestowaniu bieżącego widoku;
  • zarządzać stanem widoków dopiero po uzyskaniu działającej aplikacji w normalnych warunkach. Następnie dla każdego widoku należy uruchomić urządzenie dla różnych stanów widoku i zanotować utracone informacje. To właśnie te informacje należy zapisać, a następnie przywrócić. Następnie sprawdź nawigację: po opuszczeniu zakładki i powrocie do niej później powinna ona znajdować się w stanie, w jakim została pozostawiona;