Bez programowania Androida. Projekt z użyciem BLE i aplikacją mobilną (2) Od pomysłu, do finału

Bez programowania Androida. Projekt z użyciem BLE i aplikacją mobilną (2) Od pomysłu, do finału

W artykule prezentujemy proces powstawania urządzenia elektronicznego, działającego z interfejsem mobilnym na ekranie smartfona. Co ciekawe – nie ma potrzeby pisania aplikacji dla systemu Android, graficzny interfejs użytkownika utworzy i obsłuży samo urządzenie.

W poprzedniej części publikacji pokonaliśmy pierwsze podejście. Dzięki platformie Arduino i modułowi BBMobile szybko i sprawnie przygotowaliśmy narzędzia do projektowania i testowania interfejsów mobilnych. Zbudowaliśmy też pierwsze wersje interfejsów, które wyświetlają wartości aktualne, maksymalne i minimalne wilgotności oraz temperatury. Pora teraz na implementację kodu, który zaprezentuje informacje na ekranie urządzenia mobilnego.

Koncepcja oprogramowania

Rysunek 1. Wygląd interfejsu testowego programu

Testowe oprogramowanie przygotowane dla Arduino buduje interfejs pokazany na rysunku 1. Wyświetla wartości bieżące, maksymalne i minimalne temperatury oraz wilgotności. Kod programu składa się z trzech głównych elementów:

  • Zmiennych i danych globalnych;
  • Funkcji setup() inicjalizującej kontroler i peryferia oraz konfigurującej moduł BBMobile;
  • Funkcji loop() wykonującej pomiary, rejestrującej wartości min/max, obsługującej nadchodzące połączenia, tworzącej interfejs graficzny i odświeżającej prezentowane informacje.

Zmienne, definicje i kod JSON w tablicy pamięci programu

Na listingu 1 jako pierwszy element zdefiniowano tablicę w pamięci flash, która zawiera kod JSON zaprojektowanego w poprzedniej części publikacji interfejsu graficznego. Zawartość tej tablicy zostanie wysłana do aplikacji BBMobile za pomocą funkcji BBMobileSendJson(&BBM_SERIAL, AppJson), tuż po nawiązaniu połączenia bluetooth. Warto zauważyć, że kontrolka o nazwie t wyświetla aktualną wartość temperatury, tm minimalną, a tx maksymalną. Analogicznie rzecz ma się w przypadku wilgotności: h – aktualna wartość, hm – minimalna, hx – maksymalna wartość. Znajomość nazw tych kontrolek jest niezbędna, ponieważ polecenia sterujące po zbudowaniu interfejsu muszą zawierać nazwę kontrolki, do której zapisywać będziemy nową wartość.

Listing 1. Fragment kodu programu zawierający kod JSON zaprojektowanego interfejsu graficznego oraz najważniejsze definicje

// JSON definition of mobile interface - it is size optimized
const char AppJson[] PROGMEM ="{\"ty\":\"lout\",\"or\":\"V\",\"cs\":[\
{\"ty\":\"Textview\",\"te\":\"\nTemperatura [\u2103]\",\"tc\":\"0,0,0\",\"bg\":\"0,255,0\",\"ts\":\"30\"},\
{\"ty\":\"lout\",\"or\":\"H\",\"cs\":[\
{\"ty\":\"Textview\",\"n\":\"tm\",\"tc\":\"150,0,150\",\"ts\":\"30\",\"tf\":\"02\"},\
{\"ty\":\"Textview\",\"n\":\"t\",\"tc\":\"0,0,0\",\"ts\":\"40\",\"tf\":\"02\"},\
{\"ty\":\"Textview\",\"n\":\"tx\",\"tc\":\"150,0,150\",\"ts\":\"30\",\"tf\":\"02\"}]},\
{\"ty\":\"Textview\",\"tc\":\"0,0,0\",\"ts\":\"30\"},\
{\"ty\":\"Textview\",\"te\":\"\nWilgotno\u015B\u0107 [%]\",\"tc\":\"0,0,0\",\"bg\":\"0,255,0\",\"ts\":\"30\"},\
{\"ty\":\"lout\",\"or\":\"H\",\"cs\":[\
{\"ty\":\"Textview\",\"n\":\"hm\",\"tc\":\"150,0,150\",\"ts\":\"30\",\"tf\":\"02\"},\
{\"ty\":\"Textview\",\"n\":\"h\",\"tc\":\"0,0,0\",\"ts\":\"40\",\"tf\":\"02\"},\
{\"ty\":\"Textview\",\"n\":\"hx\",\"tc\":\"150,0,150\",\"ts\":\"30\",\"tf\":\"02\"}]},\
{\"ty\":\"Textview\",\"w\":\"5\"}]}" ;

// define Serial Monitor Port and BBMobile Serial Port
#define MON_SERIAL   Serial
#define BBM_SERIAL   bbmSerial

// software serial port for communication with BBMobile module
#define BBM_SERIAL_RX     2
#define BBM_SERIAL_TX     3
SoftwareSerial bbmSerial(BBM_SERIAL_RX, BBM_SERIAL_TX); // RX, TX

//PINS for BBMobile powering
#define BBMOBILE_GND_PIN       4
#define BBMOBILE_POWER_PIN     5

// state variable values
#define S_START         0
#define S_WAIT_CONN     1
#define S_CONNECTED     2

int state ;
unsigned int timer ;
unsigned int rh, rh_min, rh_max ;
float t, t_min, t_max ;

Kolejnymi elementami, które definiujemy poniżej są dwa porty szeregowe:

  1. MON_SERIAL – sprzętowy port, używany jako monitor, do przesyłania informacji, które następnie zostaną wyświetlone w oknie Serial Monitora na komputerze;
  2. BBM_SERIAL – programowy port szeregowy do komunikacji z modułem BBMobile.

Dalej zdefiniowane są piny programowego portu szeregowego, oraz piny, które dostarczą napięcia zasilającego dla modułu BBMobile. Dzięki temu, że pobiera on niewielkie ilości energii możliwe jest jego wygodne zasilenie wprost z portu mikrokontrolera.

Poniżej zdefiniowano jeszcze stany w jakich może znajdować się maszyna działająca w głównej części programu, co opisane zostało poniżej. Na końcu znajdują się deklaracje zmiennych, które przechowują: bieżący stan maszyny, aktualne, minimalne i maksymalne wartości temperatury oraz wilgotności, a także zmienna timer do zliczania obiegów głównej pętli programu i zarazem odmierzania czasu.

Inicjalizacja, ustawienie nazwy urządzenia i hasła dostępu

Zajrzyjmy teraz w jaki sposób nasze urządzenie po włączeniu zasilania przygotowuje się do pracy. Na listingu 2 mamy zawartość funkcji setup() przeznaczonej w koncepcji Arduino do inicjalizacji urządzenia. W pierwszej kolejności następuje odpowiednie ustawienie pinów, aby mogły pełnić zaplanowane funkcje. Następnie konfigurowane są porty szeregowe, a ich prędkość transmisji ustawiana jest na 9600 baud. Rezerwowany jest też w pamięci RAM obszar o rozmiarze 100 bajtów dla bufora bbm_buf. Bufor ten służy do przechowywania komunikatów pochodzących od modułu BBMobile.

Listing 2. Zawartość funkcji setup() przeznaczonej w koncepcji Arduino do inicjalizacji urządzenia

void setup(){
 // initialize digital pin LED_BUILTIN as an output
 pinMode(LED_BUILTIN, OUTPUT) ;

 // for powering BBMobile from defined pins
 pinMode(BBMOBILE_GND_PIN, OUTPUT) ;
 digitalWrite(BBMOBILE_GND_PIN, LOW);    //-GND for BBMobile module
 pinMode(BBMOBILE_POWER_PIN, OUTPUT) ;
 digitalWrite(BBMOBILE_POWER_PIN, HIGH); //-VCC for BBMobile module
 
 // set data rate for Monitor Serial port
 MON_SERIAL.begin(9600) ;
 while(!MON_SERIAL) ;

 // set data rate for BBmagic Serial Port
 BBM_SERIAL.begin(9600) ;
 while(!BBM_SERIAL) ;
 // reserve the number of bytes in memory for BBMobile data
 bbm_buf.reserve(100) ;  

 MON_SERIAL.println("\n\nT-RH Recorder START") ;

 //-check serial communication with BBMobile module
 //------------------------------------------------
 MON_SERIAL.println("Searching for BBMobile") ;  
 do {
   MON_SERIAL.print(".") ;    
   digitalWrite(LED_BUILTIN, HIGH);   //-turn the LED on
   delay(500) ;    
   digitalWrite(LED_BUILTIN, LOW);    //-turn the LED off
   delay(500);         
 }while(BBMobileSendWaitAck(&BBM_SERIAL, "<hello")) ;
 MON_SERIAL.println("FOUND") ;

 //-set BBMobile modules name
 //----------------------------------  
 MON_SERIAL.print("Setting mobile name - ") ;  
 if(BBMobileSendWaitAck(&BBM_SERIAL, "<name,T-RH MinMax Recorder")){
   MON_SERIAL.println("err") ;
 }else MON_SERIAL.println("OK") ;

 //-set BBMobile modules PIN
 //----------------------------------
 MON_SERIAL.print("Setting PIN - ") ;
 if( BBMobileSendWaitAck(&BBM_SERIAL, "<pin,234") ){  //-set PIN to 234
 //MON_SERIAL.print("Setting no PIN - ") ;
 //if( BBMobileSendWaitAck(&BBM_SERIAL, "<pin,0") )  //-delete PIN
   MON_SERIAL.println("err") ;
 }else MON_SERIAL.println("OK") ;

 //-init values
 t= get_t() ;
 t_min= t_max= t ;

 rh= get_rh() ;
 rh_min= rh_max= rh ;
}

Po tych wstępnych ustawieniach kontroler sprawdza w pętli, czy komunikacja z BBMobile działa poprawnie. Pętla jest opuszczana dopiero wówczas gdy funkcja BBMobileSendWaitAck(&BBM_SERIAL, "<hello") zwróci wartość zero, co oznacza, że na wysłane pozdrowienie <hello nadeszła odpowiedź >HI. Po opuszczeniu pętli następuje krótka konfiguracja modułu.

Rysunek 2. Skanowanie w poszukiwaniu dostępnych urządzeń Bluetooth

Najpierw ustawiamy nazwę urządzenia, która pojawi się na liście wyboru w aplikacji podczas skanowania w poszukiwaniu dostępnych urządzeń Bluetooth (rysunek 2). Dokonujemy tego wysyłając komendę <name,T-RH MinMax Recorder za pomocą funkcji

Pomiary, rejestracja min/max i sygnalizacja LED

Pierwsza część kodu głównej pętli programu została zawarta na listingu 3. Dzięki instrukcji delay(50) pętla wykonywana jest ok. 20 razy na sekundę, a każdorazowo inkrementowana zmienna timer pozwala zliczać jej przebiegi i odmierzać dłuższe odcinki czasu. Dalej znajduje się kod, który co 1 sekundę wykonuje pomiary, uaktualnia wartości maksymalne i minimalne oraz sygnalizuje diodą LED poprawne działanie urządzenia. Zadania te wykonywane są niezależnie od tego czy połączenie z aplikacją mobilną jest zestawione czy też nie.

Listing 3. Pierwsza część kodu głównej pętli programu

 delay(50) ;
 timer++ ;

 // LED blinking
 if(timer ==4) digitalWrite(LED_BUILTIN, LOW) ;  //-turn the LED off
 if(timer > 20) {
   timer =0 ;
   digitalWrite(LED_BUILTIN, HIGH);  //-turn the LED on

   //-read temperature:
   t= get_t() ;
   // calculate new min and max temperatures
   if(t_min > t) t_min= t ;
   if(t_max < t) t_max= t ;
       
   //-read humidity:    
   rh= get_rh() ;
   // calculate new min and max rh
   if(rh_min > rh) rh_min= rh ;
   if(rh_max < rh) rh_max= rh ;
 }

Dzięki temu wartości maksymalne i minimalne rejestrowane są ciągle, a po połączeniu z rejestratorem na ekranie smartfona dostępne są najświeższe wykonane pomiary. Implementacja funkcji pomiarowych get_t() i get_rh() zależy oczywiście od zastosowanego układu pomiarowego. W naszym przypadku ich implementowanie byłoby bezcelowe, ponieważ łatwe przeniesienie kodu na platformę docelową nie jest możliwe. Poza tym w tej części projektu kładziemy nacisk na przygotowanie interfejsu mobilnego i jego obsługi.

Maszyna stanów głównej pętli programu

Druga część kodu głównej pętli programu pokazana jest na listingu 4. Implementuje maszynę stanów utworzoną w oparciu o instrukcję switch(), która realizuje budowę i obsługę mobilnego interfejsu użytkownika. Kod maszyny wykonywany jest przy każdym obiegu pętli głównej, co 50 milisekund. Zmienna state przechowuje aktualny stan maszyny czyli określa, która część kodu zostanie wykonana przy aktualnym obiegu pętli. Może przyjmować jedną z trzech wartości: S_START, S_WAIT_CONN lub S_CONNECTED. Maszyna rozpoczyna swoje działanie od stanu S_START, w którym wysyła do Serial Monitora komunikat informujący o rozpoczęciu oczekiwania na zestawienie połączenia (rysunek 3).

Listing 4. Druga część kodu głównej pętli programu

 switch(state){
   case S_START:
     MON_SERIAL.print("Waiting for BLE connection...") ;
     state =S_WAIT_CONN ;
   break ;
   
   case S_WAIT_CONN: //-no BLE connection - waiting
     BBMobileGetMessage(&BBM_SERIAL) ;
     if(BBMobileIsConnected() != 0){
       MON_SERIAL.print("App Connected\nSending JSON...") ;
       if(BBMobileSendJson(&BBM_SERIAL, AppJson)){
         MON_SERIAL.println("err") ;
       }else{
         MON_SERIAL.println("OK\nPlaying with interface...") ;
       }
       state =S_CONNECTED ;
     }
   break ;

   case S_CONNECTED: //-connected to mobile App
     //-get messages from mobile interface
     //------------------------------------
     while(BBMobileGetMessage(&BBM_SERIAL) > 0){} ;
         
     if(BBMobileIsConnected() == 0){
       MON_SERIAL.println("App disconnected") ;
       state =S_START ;
     }else{
       //-send data to mobile interface
       //------------------------------------
       if(timer ==2){
         BBMobileSendWaitAck(&BBM_SERIAL, "$set,tm:te=\""+String(t_min, 1)+"\",t:te=\""+String(t, 1)+"\",tx:te=\""+String(t_max, 1)+"\"") ;
         BBMobileSendWaitAck(&BBM_SERIAL, "$set,hm:te=\""+String(rh_min, DEC)+"\",h:te=\""+String(rh, DEC)+"\",hx:te=\""+String(rh_max, DEC)+"\"") ;
       }
     }
   break ;
   
   default:
   break ;
 } ;

const char AppJson[] PROGMEM ="{\"ty\":\"lout\",\"or\":\"V\",\"cs\":[\
{\"ty\":\"Textview\",\"te\":\"\nTemperatura [\u2103]\",\"tc\":\"0,0,0\",\"bg\":\"0,255,0\",\"ts\":\"30\"},\
{\"ty\":\"lout\",\"or\":\"H\",\"cs\":[\
{\"ty\":\"Textview\",\"n\":\"tm\",\"tc\":\"150,0,150\",\"ts\":\"30\",\"tf\":\"02\"},\
{\"ty\":\"Textview\",\"n\":\"t\",\"tc\":\"0,0,0\",\"ts\":\"40\",\"tf\":\"02\"},\
{\"ty\":\"Textview\",\"n\":\"tx\",\"tc\":\"150,0,150\",\"ts\":\"30\",\"tf\":\"02\"}]},\
{\"ty\":\"Textview\",\"tc\":\"0,0,0\",\"ts\":\"30\"},\
{\"ty\":\"Textview\",\"te\":\"\nWilgotno\u015B\u0107 [%]\",\"tc\":\"0,0,0\",\"bg\":\"0,255,0\",\"ts\":\"30\"},\
{\"ty\":\"lout\",\"or\":\"H\",\"cs\":[\
{\"ty\":\"Textview\",\"n\":\"hm\",\"tc\":\"150,0,150\",\"ts\":\"30\",\"tf\":\"02\"},\
{\"ty\":\"Textview\",\"n\":\"h\",\"tc\":\"0,0,0\",\"ts\":\"40\",\"tf\":\"02\"},\
{\"ty\":\"Textview\",\"n\":\"hx\",\"tc\":\"150,0,150\",\"ts\":\"30\",\"tf\":\"02\"}]},\
{\"ty\":\"Textview\",\"w\":\"5\"}]}" ;

{"ty":"lout","or":"V","cs":[
{"ty":"Textview","te":"\nTemperatura [\u2103]","tc":"0,0,0","bg":"0,255,0","ts":"30"},
{"ty":"lout","or":"H","cs":[
{"ty":"Textview","n":"tm","te":"19,2","tc":"150,0,150","ts":"30","tf":"02"},
{"ty":"Textview","n":"t","te":"20,1","tc":"0,0,0","ts":"40","tf":"02"},
{"ty":"Textview","n":"tx","te":"23,8","tc":"150,0,150","ts":"30","tf":"02"}]},
{"ty":"Textview","tc":"0,0,0","ts":"30"},
{"ty":"Textview","te":"\nWilgotno\u015B\u0107 [%]","tc":"0,0,0","bg":"0,255,0","ts":"30"},
{"ty":"lout","or":"H","cs":[
{"ty":"Textview","n":"hm","te":"45 %","tc":"150,0,150","ts":"30","tf":"02"},
{"ty":"Textview","n":"h","te":"55 %","tc":"0,0,0","ts":"40","tf":"02"},
{"ty":"Textview","n":"hx","te":"59 %","tc":"150,0,150","ts":"30","tf":"02"}]},
{"ty":"Textview","w":"5"}]}

Zestawienie połączenia i budowanie interfejsu

Najwięcej czasu maszyna spędza wykonując kod stanu S_WAIT_CONN, gdzie oczekuje, aż użytkownik nawiąże połączenie. Najpierw za pomocą funkcji BBMobileGetMessage(&BBM_SERIAL) sprawdza czy nadeszły nowe dane od modułu BBMobile. Kolejno wywoływana funkcja BBMobileIsConnected() sprawdza czy połączenie bluetooth zostało zestawione. Po uruchomieniu na urządzeniu mobilnym aplikacji BBMobile dotykamy START w prawym górnym rogu. Wybieramy z listy nasze urządzenie (rysunek 2) i wprowadzamy PIN (rysunek 4). Gdy jest on poprawny wówczas połączenie zostanie zestawione, a funkcja zwróci wartość jeden, co implikuje konieczność zbudowania interfejsu.

Rysunek 4. Okno służące wprowadzeniu kodu PIN

Wówczas, zapisany w tablicy const char AppJson[] PROGMEM kod JSON jest niezwłocznie wysyłany za pomocą funkcji BBMobileSendJson(&BBM_SERIAL, AppJson). Rysunek 5 pokazuje kolejne komunikaty, które pojawiły się w oknie Serial Monitora po połączeniu i zbudowaniu interfejsu. Wygląd ekranu smartfona prezentuje znany nam już rysunek 1, a programowa maszyna przechodzi do kolejnego stanu, w którym zajmie się aktualizowaniem wyświetlanych przez interfejs informacji.

Rysunek 5. Komunikaty, które pojawiły się w oknie Serial Monitora po połączeniu i zbudowaniu interfejsu

Cykliczne aktualizowanie wyświetlanych wartości

W stanie S_CONNECTED przy każdym obiegu pętli głównej za pomocą funkcji BBMobileGetMessage(&BBM_SERIAL) odbieramy nadchodzące dane. Następnie przy użyciu BBMobileIsConnected() sprawdzamy czy połączenie z aplikacją jest nadal zestawione. Jeśli tak, to co sekundę (if(timer ==2)) wysyłamy do interfejsu dwie komendy, które aktualizują wszystkie sześć wyświetlanych wartości. Składnia komend sterujących zaprezentowana została w poprzedniej części cyklu, a więcej informacji można znaleźć na witrynie www.bbmagic.net. W tym kodzie wartości zmiennych t_min, t, t_max, rh_min, rh, oraz rh_max zamieniamy na ciągi tekstowe za pomocą funkcji String(..). Następnie umieszczamy je w tekstowej komendzie BBMobile i wysyłamy do interfejsu funkcją BBMobileSendWaitAck(..). W ten sposób informacje na ekranie smartfona są odświeżane co sekundę.

Rysunek 6. Powrót maszyny do pierwszego stanu jest sygnalizowane odpowiednim komunikatem

Wysyłanie kolejnych poleceń sterujących dla zbudowanego interfejsu jest kontynuowane do chwili naciśnięcia w aplikacji przycisku STOP, co spowoduje zamknięcie połączenia i zwrócenie wartości zero przez funkcję BBMobileIsConnected(). Wówczas następuje wyświetlenie komunikatu App disconnected i wpisanie do zmiennej state wartości S_START. Spowoduje to, w kolejnym obiegu głównej pętli programu, powrót maszyny do pierwszego stanu i wysłanie na ekran monitora kolejnego komunikatu, jak pokazuje rysunek 6. Nasza maszyna jest znów gotowa na zestawienie nowego połączenia. Gdy to nastąpi cały proces budowania i obsługi interfejsu powtórzy się na nowo.

Co pozostało do zrobienia

W kolejnej, ostatniej już części publikacji, rozbudujemy kod JSON interfejsu tak, aby spełniał wszystkie początkowe założenia, omówimy docelową platformę sprzętową i zaprezentujemy działanie finalnego programu.

Mariusz Żądło

Artykuł ukazał się w
Elektronika Praktyczna
lipiec 2021

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik maj 2022

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio maj - czerwiec 2022

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka Podzespoły Aplikacje kwiecień 2022

Automatyka Podzespoły Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna maj 2022

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich maj 2022

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów