Oryginalny projekt Piotra Rosenbauma zawierał skompilowane oprogramowanie pod system MS Windows. Opis dostarczony przez autora koncentrował się na części sprzętowej, a szczegóły dotyczące sposobu wymiany danych poprzez interfejs Bluetooth były niekompletne.
Jednakże zastosowanie oprogramowanie do monitorowania komunikacji przez interfejs szeregowy umożliwiło pełny reverse-engineering zastosowanego, prostego protokołu komunikacyjnego i odtworzenie przesyłanych komunikatów w nowym programie.
Aplikacja pod system Android nie została jedna stworzona w Javie, czyli w natywnym języku, z którego korzysta system operacyjny, ale z użyciem platformy Apache Cordova. Platforma ta stanowi bezpłatny zbiór bibliotek, tłumaczących polecenia javascriptowe na polecenia języków stosowanych natywnie w mobilnych systemach operacyjnych - w naszym przypadku - w Javy.
Cała platforma Cordova obejmuje też kompilatory i szereg pluginów. Korzystania z Cordovy można nauczyć się dzięki publikowanemu w Elektronice Praktycznej od lutego 2015 roku kursowi programowania aplikacji mobilnych. Niemal cała wiedza potrzebna do realizacji niniejszej aplikacji zawarta jest w pierwszych pięciu częściach kursu, przy czym w opisywanym programie, na potrzeby uzyskania odpowiedniego wyglądu aplikacji, skorzystano z języków HTML i CSS w stopniu wykraczającym poza zakres informacji ze wspomnianych części kursu.
Warto dodać, że udostępniamy czytelnikom nie tylko gotową, skompilowaną aplikację androidową, ale też jej pełny kod źródłowy, dzięki czemu każdy będzie mógł samodzielnie zmodyfikować wygląd i funkcje aplikacji, skompilować ją, a być może nawet przenieść na inny system operacyjny.
Opis protokołu
Protokół komunikacyjny, zastosowany w zestawie AVT5295 jest bardzo prosty i polega na przesyłaniu pojedynczych znaków lub ciągów znaków przez interfejs Bluetooth. Na początek urządzenie mobilne powinno samo nawiązać połączenie z wykrytym urządzeniem Bluetooth o nazwie "Bluetooth Adaptor".
Następnie zestaw z przekaźnikami zaczyna cyklicznie, co 1 sekundę, przesyłać ciąg znaków, zawierający aktualną temperaturę, odczytywaną z wbudowanego sensora. Każdy taki komunikat zakończony jest znakiem końca linii, takim jaki stosuje się w systemie Windows, czyli bajtami 0x0D i 0x0A, określanymi też mianem znaków CR i LF.
W systemie Windows te dwa następujące po sobie bajty są traktowane jako jeden znak końca linii, choć w praktyce są to następujące po sobie znaki "r" i "n". Gdyby chcieć przenosić aplikację na inny system, należy wziąć pod uwagę fakt, że w Linuksie jako znak końca linii używany jest tylko 0x0A, a w systemach MacOS - tylko 0x0D. Ale mniejsza z tym.
W naszym przypadku informacja o temperaturze zajmuje najczęściej 6 bajtów, na które składają się następujące znaki: dwie cyfry przed przecinkiem, znak przecinka (autor użył w tym celu kropki), jedna cyfra po przecinku i wspomniane dwa znaki końca linii (CR i LF). To jedyne dane, jakie moduł Bluetooth Relay przesyła do aplikacji.
Aplikacja natomiast steruje pracą modułu poprzez wysyłanie pojedynczych liter alfabetu. Moduł obsługuje 14 różnych poleceń, z których każde reprezentowane jest przez inny znak. Małe litery od "a" do "l" służą do włączania i wyłączania kolejnych przekaźników, a znaki "y" i "z" do włączania i wyłączania (odpowiednio) wszystkich przekaźników na raz. Pełna lista komend przesyłanych do modułu została zawarta w tabeli 1.
Wszystkie operacje związane z opóźnionym włączaniem lub wyłączaniem przekaźników oraz z kontrolą ich stanu są wykonywane w aplikacji. Aplikacja nie ma podglądu na rzeczywisty stan przekaźników ani nie może wysłać polecenia zmiany stanu przekaźnika za jakiś czas, np. po jej wyłączeniu. Prezentowany w aplikacji stan przekaźników jest kalkulowany w oparciu o polecenia wydawane przez użytkownika.
Konstrukcja aplikacji
Przygotowana aplikacja wykorzystuje oprogramowanie Apache Cordova i zawartą w niej platformę androidową. Ponadto użyto dwóch pluginów:
- com.megster.cordova.bluetoothserial, potrzebnego do realizacji komunikacji przez interfejs Bluetooth,
- org.apache.cordova.dialogs, ułatwiającego prezentowanie okien dialogowych.
Program opiera się o standardową, czystą aplikację Cordovy, w której zmieniono jedynie treść plików index. js, index.html i index.css. Ponadto skorzystano z biblioteki jQuery, którą dodano do zasobów w katalogu z plikami Javascripta oraz wprowadzono ikonkę, by zastąpić domyślną ikonę aplikacji Cordovy.
Działanie aplikacji rozpoczyna się od przypisania poleceń do wszystkich przycisków oraz inicjalizacji interfejsu Bluetooth. Po nawiązaniu połączenia z modułem z przekaźnikami, uruchamiane jest nasłuchiwanie na ciągi znaków, przesyłane interfejsem Bluetooth i zakończone znakami końca linii (CR i LF). Pozyskiwane ciągi znaków są prezentowane jako wskazanie temperatury, w górnej części aplikacji.
W przypadku naciśnięcia któregokolwiek z przycisków, aplikacja wysyła adekwatną komendę do modułu z przekaźnikami lub wyświetla okno dialogowe z prośbą o wskazanie opóźnienia, z jakim ma być wysłana dana komenda. Ponieważ logika programu jest napisana z użyciem języka Javascript, bardzo łatwo było zrealizować asynchroniczną pracę aplikacji.
W efekcie, nic nie stoi na przeszkodzie, by zażądać wykonania kilku opóźnionych operacji dla tego samego przekaźnika, wywoływanych w dowolnym czasie. Każda kolejna zlecona, opóźniona operacja nie wstrzymuje wykonywania poprzedniej, choć wyświetlany timer prezentuje odliczanie w dosyć specyficzny sposób: z upływem każdej jednostki czasu (godziny, minuty, sekundy), w jakiej podane było opóźnienie wykonania danej komendy, zmieniane jest wskazanie timera dla danego przekaźnika.
Jeśli przykładowo, dla przekaźnika pierwszego, prowadzone są jednocześnie trzy odliczania, liczby prezentujące pozostały czas dla poszczególnych timerów będą prezentowane naprzemiennie.
Oprócz tej różnicy z harmonogramowaniem opóźnionych komend dla przekaźników, działanie aplikacji mobilnej jest identyczne z działaniem oryginalnego programu dla systemu operacyjnego MS Windows. Naturalnie istotnie różny jest graficzny interfejs użytkownika.
Szczegóły implementacji HTML
Na samym początku sekcji BODY znajduje się warstwa "connection", która informuje o aktualnym stanie połączenia Bluetooth. Poniżej znalazło się pole w które wpisywana jest wartość temperatury podana przez moduł z przekaźnikami (pole "temp"). Następnie umieszczono dwa duże przyciski "Włącz" i "Wyłącz", które sterują wszystkimi przekaźnikami jednocześnie.
W dalszej części umieszczono powtarzające się tabele klasy "relay" - po jednej dla każdego przekaźnika. Ich pola (przyciski i spany) różnią się tylko ostatnią cyfrą, która pozwala rozpoznać, którego przekaźnika dotyczą. Każdy z przekaźników obsługiwany jest przez cztery przyciski: dwa do natychmiastowego włączenia i wyłączenia ("now-on-X" i "now-off-X", gdzie X to numer przekaźnika) oraz dwa do opóźnionego włączenia i wyłączenia ("later-on-X" i "later-off-X").
Pole "state-X" informuje o aktualnym stanie danego przekaźnika, a pole "time-X" wskazuje czas pozostały w timerach powiązanych z danym przekaźnikiem. Dzięki zastosowaniu wtyczki org.apache.cordova.dialogs, nie było konieczne samodzielne tworzenie okien dialogowych, potrzebnych do wpisywania czasu opóźnienia wysłania komend. Fragment pliku index.html został pokazany na listingu 1.
Szczegóły implementacji CSS
Plik CSS bazuje na standardowym pliku stylu do świeżej aplikacji Cordovy, pozbawionym kilku niepotrzebnych elementów. Zmieniono m.in. parametry nagłówka H1 oraz wykorzystano klasę .blink wraz ze zmodyfikowanymi parametrami aplikacji, by sygnalizować że trwa nawiązywanie połączenia Bluetooth.
Elementom tabeli i przyciskom (TABLE, TD, BUTTON) przypisano odpowiednie rozmiary i wielkości czcionek, tak by nie pozostawiały pustego miejsca na ekranie. Dodano też klasy:
- .relay-on - by kolorem oznaczyć przekaźniki włączone,
- .timed-on - by kolorem zielonym oznaczać odliczanie timera powodującego włączenie przekaźnika,
- .timed-on - by kolorem czerwonym oznaczać odliczanie timera powodującego wyłączenie przekaźnika. Fragment pliku index.css znalazł się na listingu 2. Szczegóły implementacji Javascript
Naturalnie najbardziej złożony jest kod Javascriptu. W funkcji app.onDeviceReady() umieszczono wywołania dwóch funkcji:
- app.bt1(), która inicjalizuje połączenie Bluetooth z modułem z przekaźnikami oraz prowadzi nasłuch danych dotyczących temperatury,
- app.assign_buttons(), która powoduje przypisanie akcji do poszczególnych przycisków.
Oprócz tego stworzono funkcję app.countdown(), która służy do prezentacji czasu pozostałego do zakończenia odliczania czasu timerami.
Funkcja app.bt1() została pokazana na listingu 3. Rozpoczyna się od zmiany wyglądu paska informującego o stanie interfejsu Bluetooth. Następnie aplikacja wykrywa urządzenia Bluetooth w otoczeniu i gdy skończy, zmienia pasek informujący o stanie interfejsu oraz zaczyna, dla każdego ze znalezionych urządzeń, sprawdzać czy nazwa urządzenia to "Serial Adaptor".
Jeśli tak się stanie, aplikacja nawiązuje z danym urządzeniem połączenie, zmienia stan pasku informującego o interfejsie BT i zaczyna nasłuchiwać, korzystając z polecenia bluetoothSerial.subscribe() na ciągi znaków zakończone znakiem końca linii. Po każdym odebranym pakiecie takich danych, treść pola "temp" w aplikacji podmieniana jest na nowe wskazanie temperatury.
Funkcja app.assign_buttons() składa się w większości z powtarzających się elementów. Dla każdego z przycisków definiowane jest odpowiednie przypisanie akcji następującej po jego naciśnięciu. Jest to przesłanie odpowiedniej komendy przez interfejs Bluetooth oraz zmiana etykietki ze stanem adekwatnych przekaźników.
Fragment tego kodu przedstawiono na listingu 4. Warto zwrócić uwagę na to, że w przypadku przycisków włączających i wyłączających wszystkie przekaźniki jednocześnie, zmiana sygnalizacji stanu przekaźników jest realizowana w oparciu o klasę .state, a nie o unikalne identyfikatory.
Znacznie ciekawszy jest kod służący do przypisania akcji przyciskom zwłocznym (listing 5). Został on tak zoptymalizowany, by zajął jak najmniej miejsca - wszystkim tym przyciskom (klasy .later) przypisywana jest jedna i ta sama funkcja. Wstępnie jednak pobierany jest identyfikator danego przycisku, dla którego funkcja została wywołana. Identyfikator ten pozwala później rozpoznać, którego przekaźnika dotyczy dana operacja.
Dla ułatwienia, wykorzystano polecenie navigator. notification.prompt(), które wywołuje okno do wprowadzania czasu opóźnienia wysłania komendy. Na dole okna umieszczono trzy przyciski, odpowiadające trzem jednostkom podawanej wartości opóźnienia: godzinom, minutom i sekundom.
Naciśnięcie jednego z tych przycisków powoduje wywołanie kolejnej funkcji, w której obliczany jest odpowiedni mnożnik czasu dla wybranej jednostki: 3600, 60 lub 1. W przypadku gdy użytkownik nie naciśnie żadnego z tych przycisków, tylko zamknie okienko dialogowe, korzystając z systemowego przycisku "cofnij", timer nie zostanie ustawiony.
Timer nie zostanie też ustawiony, jeśli wartość wpisana w oknie dialogowym nie jest liczbą całkowitą. Obliczona liczba sekund (a następnie milisekund), jaka musi upłynąć przed wysłaniem komendy jest podawana w funkcji setTimeout().
Aby skrócić ilość kodu potrzebnego do obsługi przycisków zwłocznych, z zapamiętanego wcześniej identyfikatora wycinane są fragmenty określające numer przekaźnika, którego dotyczy przycisk oraz rodzaj wywoływanej operacji (włączenie lub wyłączenie).
By uniknąć problemu ponownego definiowania komend Bluetooth, odpowiadających poszczególnym operacjom, w funkcji uruchamianej po żądanym czasie zwłoki wyzwalane jest kliknięcie w przycisk odpowiadający danej operacji i danemu przekaźnikowi; przyciskom tym przypisano bowiem już odpowiednie komendy we wcześniejszym fragmencie kodu funkcji app.assign_buttons().
Na koniec, już poza funkcją setTimeout() wywoływana jest funkcja app.countdown() z parametrami określającymi numer przekaźnika, rodzaj operacji oraz wybrane jednostkę czasu i czas zwłoki. Funkcja app.countdown() jest praktycznie niezależna i służy tylko temu, by w odpowiednich miejscach interfejsu prezentować odliczanie czasu timerów. Funkcja ta została zaprezentowana na listingu 6.
Działanie funkcji app.countdown() polega na sprawdzaniu, czy pozostały czas nie osiągnął zera i w razie potrzeby na zmniejszeniu licznika czasu o 1, wpisaniu nowej wartości w odpowiednim polu interfejsu użytkownika oraz na ponownym zaplanowaniu wywołania siebie samej, z wykorzystaniem funkcji setTimeout(), z opóźnieniem odpowiadającym jednej jednostce czasu, jaką użytkownik wybrał dla danego timera.
Podsumowanie
Gotowa aplikacja zajmuje niecałe 2 MB i uruchamia się praktycznie błyskawicznie. Pozwala zastąpić program w wersji na Windows. Dodatkowo, przenośny kod sprawia, że bardzo łatwo dokonać kompilacji kodu dla innych systemów operacyjnych, takich jak Windows Phone 8 i iOS, z tym że trzeba mieć na uwadze, że niektóre urządzenia mogą mieć problemy z komunikacją z użyciem interfejsu Bluetooth.
Marcin Karbowniczek, EP