Wysyłanie poleceń z Raspberry do tinyBrd

Dotychczas tinyBrd wysyłało przez NRF24L01 dane do Raspberry. Zarówno gdy testowaliśmy jakość połączenia jak i gdy wysyłaliśmy dane z DS18B20. Tym razem na warsztat weźmiemy odwrotny przykład.

Plan jest następujący: Raspberry będzie wysyłało żądanie odczytu stanu wejścia analogowego w tinyBrd a po otrzymaniu stanu będzie wysyłać polecenie zapalenia odpowiedniej ilości diod podłączonych do tinyBrd.

Plan jest znany, to może pokażemy co wyszło finalnie:

Pierwsze podejście do implementacji jest takie: RPi wysyła pakiet z jednym bajtem o wartości 99, co znaczy 'podaj mi stan wejścia analogowego’, tinyBrd odeśle wtedy wartość.

Raspberry odbierze dane od tinyBrd i wyśle dwa bajty do tinyBrd, pierwszy to 102 (taką wartość ma polecenie zapal diody) a drugi ile diod ma zostać zaświeconych. tinyBrd odbierając komendę 102, zaświeci tyle diod ile będzie podane w drugim z bajtów.

Proste?  Wartości 'komend’ zostały wybrane arbitralnie, teraz omówię szczegóły.

Przyjrzyjmy się szkicowi na tinyBrd, na początek weźmy zmienne globalne:

byte leds[4] = { 1, 2, 8, 9};
byte serverAddr[3] = { 0, 0, 3 };
byte address[3] = { 3, 0, 0 };

Tablica leds przechowuje numery portów cyfrowych, do których podpięte są diody LED. serverAddr to adres modułu NRF podłączonego do RPi, a address to adres modułu na tinyBrd.

struct Payload
{
  byte id;
  unsigned int payload;
} data;

Struktura Payload służyć będzie do 'odsyłania’ do RPi wartości odczytanej z wejścia analogowego. Nasz przykład jest oparty o jedno tinyBrd, a pomysł tej struktury jest zaczerpnięty z wcześniejszego przykładu na Akademii o testowaniu połączenia tinyBrd. Pamiętajmy, że pakiet przekazywany przez NRF nie zawiera żadnego nagłówka np z adresem nadawcy. Dlatego, jeżeli chcemy korzystać z więcej niż dwóch modemów NRF w jednej sieci, warto dodać jakiś identyfikator wewnątrz danych. Tutaj to jest bajt id.

Co w setup?

  Radio.begin(address, 100);
  data.id = 1;
  for (byte i = 0; i < 4; i++) {
    pinMode(leds[i], OUTPUT);
  }

Po pierwsze, aktywujemy radio na kanale 100 i używamy adresu ustawionego w address, jako adres na którym nasłuchuje. Ustawiamy też id na 1,  oraz wszystkie wyjścia z diodami LED na tryb pracy OUTPUT.

Loop…

  if (byte cnt = Radio.available()) {
    byte cmd[cnt];
    Radio.read(cmd);

Kluczową częścią pętli loop jest sprawdzenie czy odebrane zostały dane. Liczba odebranych bajtów jest zwracana przez Radio.available() i jeżeli jest różna od zera, tworzymy tymczasową zmienną cmd o rozmiarze by pomieścić wszystkie dane i możemy zacząć ich analizę – po zapisaniu ich w tej zmiennej przez Radio.read(cmd).

Jak w założeniach podawałem, pierwszy bajt zawiera kod komendy. 99 – podaj odczyt wejścia analogowego, 102 – ustaw diody LED. Także najprostszym podejściem będzie:

switch (cmd[0]) { 
case 99:
  //wyślij dane 
  break;
case 102:
  //odbierz dane
  //ustaw diody
  break;
}

Jeśli nie jest znana Tobie konstrukcja switch to warto się z nią zapoznać, może ona zastąpić wielopiętrowe kaskady if/else.

Co warto o switch pamiętać? Argument do switch to właśnie testowana wartość. Tutaj mamy cmd[0] czyli pierwszy bajt z odebranych przez NRF. Zgodnie z założeniem jest to nasza komenda. Każde polecenie case porównuje podaną wartość i jeżeli są równe, to rozpoczyna wykonanie kodu następującego po case. Warto pamiętać, że na końcu każdej sekcji case trzeba dać komendę break, bez niej następna sekcja też zostanie wykonana. W powyższym kodzie, jeżeli usunąć komendę break to przy wartości cmd[0] równej 99 wykona się zarówno to co jest po case 99: jak i po case 102:

Dobrze, po kolei, case 99: czyli wyślij odczyt:

      case 99:
        int ret;
        data.payload = analogRead(A0);
        Radio.write(serverAddr, data);
        ret = Radio.flush(RADIO_BLOCK);
        switch (ret) {
          case RADIO_LOST:
            debug.println(F("NOT SENT"));
            break;
          case RADIO_SENT:
            debug.println(F("gone..."));
            break;
          default:
            debug.print(F("OTHER!!! "));
            debug.println(ret, DEC);
        }
        break;

Najpierw w data.payload umieszczamy wartość odczytaną z potencjometru, potem wysyłamy do modemu (Radio.write) i czekamy na rezultat (Radio.flush). Argument RADIO_BLOCK to wartość domyślna i można go pominąć – funkcja flush nie skończy swojego działania dopóki pakiet nie zostanie doręczony, lub minie czas na to przeznaczony i pakiet zostanie uznany za stracony.

Wartość zwracana przez flush w tej wersji jest nam tylko potrzebna do tego by wysłać komunikat na debug.

No to zostaje nam zapalenie diod:

      case 102:
        for (byte j = 1; j <= 4; j++) {
          bool val = j <= cmd[1];
          digitalWrite(leds[j - 1], val);
        }
        break;

 Tu jest prosta sprawa, jeżeli polecenie odebrane przez tinyBrd to 102, znaczy się, że następny bajt (cmd[1]) zawiera liczbę, ile diod trzeba zapalić. Wystarczy prosta pętla, od 1 do 4, jeżeli liczba wysłana przez RPi to 0, żadna dioda się nie zapali – val będzie false, bo zmienna pętli (j) będzie zawsze większa.

Tablica leds służy jako tłumacz między kolejnymi numerami a numerami wyjść do których podpięte są diody.

Trzeba pamiętać, że w Arduino (czy ogólnie w C/C++) tablice są zawsze indeksowane od 0, czyli pierwszy element tablicy leds to leds[0]. Dlatego przy ustalaniu wyjścia stan HIGH/LOW trzeba odjąć jeden od j.

W digitalWrite wartość HIGH odpowiada wartości logicznej true, dlatego możemy użyć zmiennej val.

Co z Raspberry?

Korzystamy z naszej biblioteki do NRFa dla Raspberry, szczegóły instalacji można poznać na Akademii Nettigo. Mając wszystko zainstalowane, korzystamy ze skryptu w Pythonie do odbierania danych i wysyłania poleceń. Omówię teraz kilka elementów  z których składa się program.

def setup_radio():
    radio = Radio(bytes([0,0,3]),100)
    return radio


Funkcja setup_radio ustawia parametry radia NRF24L01, oczywiście adres musi się zgadzać z tym, co wpisane jest w szkicach na tinyBrd.

Główna pętla oparta jest o funkcję read_data,  która wykonuje w pętli zadanie wysyłania poleceń do tinyBrd oraz odbiera dane. Zaczniemy od tego drugiego zadania.

        if radio.available():
            data = radio.read()
            response = True   
            if (len(data)==4):
                value = struct.unpack('=BHB', data)
                                    # 0 - id
                                    # 1 - potentiometer
                                    # 2 - heartbeat
                sensor_id = value[0]
                pot = value[1]
                hb = value[2]
                if (hb == 0):
                    mapped = int(map_value(pot,0,1023,0,4)).to_bytes(2,byteorder='little')[0]
                    print_with_time ("ID:{}: AnalogRead:{}, map {}".format(sensor_id,pot,mapped))
                    set_led(radio,mapped)
                else:
                    print_with_time("ID:{}: HearBeat received".format(sensor_id))

 Jeśli dostaliśmy odpowiedź (jakieś dane są do odebrania), to zapamiętujemy fakt otrzymania odpowiedzi (response = True) oraz gdy liczba bajtów zgadza się z tym co spodziewaliśmy się dostać, to rozpakowujemy otrzymane dane struct.unpack('=BHB', data) Po tej operacji będziemy mieli tablicę, pasującą do struktury Payload z tinyBrd – bajt, liczbę całkowitą i bajt. Pierwszy bajt to tradycyjnie nr (ID) tinyBrd (na wypadek gdy będziemy mieli kilka w sieci), druga wartość to odczyt z wejścia analogowego. Bajt ostatni to tak zwany dowód życia :) heart beat, który był wykorzystywany podczas różnych testów, tutaj można go zignorować.

Wartość zwrócona przez unpack to tablica, dla naszej własnej wygody przepisujemy to do zmiennych, których nazwy znaczą coś dla nas. Róbcie tak z dwóch powodów. Trudniej o pomyłkę później w kodzie, gdy użyjemy złego indeksu. Drugi powód to łatwość zmian. Gdybyśmy zmienili kolejność parametrów w strukturze, wówczas wystarczy zmienić w jednym miejscu, tylko podczas przypisania do zmiennej.

Ze względu na HB pojawia się nam dodatkowy if, bowiem pakiet z hb różnym od zera ma być ignorowany. Gdy hb jest zero, wówczas wartość potencjometru jest przypisywana za pomocą funkcji mapped_value na liczbę z przedziału 0-4. Czyli ile diod ma zostać zapalonych przez tinyBrd. mapped_value jest implementacją funkcji map z Arduino. Przeniosłem to na Raspberry, by wyraźniej pokazać, że to właśnie na nim odbywa się przetwarzanie danych, a tinyBrd będzie tylko zajmować się wyświetlaniem.

Wartość zwróconą przez mapped_value wysyłamy do tinyBrd używając funkcji set_led:

def set_led(r,nr):
    b=bytes([102,nr])
    r.write(remote,b)
    r.flush()

Jak wcześniej było ustalone 102, to kod polecenia zapalania diod, a nr to po prostu ile diod zapalić. W zasadzie, to możemy użyć funkcji pack o odwrotnym działaniu niż unpack, ale ponieważ dane do wysłania są bardzo proste, to przygotujemy dane za pomocą funkcji bytes, podając jako argumenty nasze dwie wartości.

Cała wysyłka to kombinacja write/flush – tym razem liczymy na to, że pakiet dojdzie. Jeżeli nie, to straty dużej nie ma – cały kod działa w pętli i za chwilę zostanie tinyBrd odpytany jeszcze raz.

To tyle jeżeli chodzi o funkcję wysyłającą polecenie zapalenia diod, funkcja wysyłająca polecenie podaj stan potencjometru wygląda podobnie:

def read_analog(r):  
    b = bytes([99])  
    r.write(remote,b)
    r.flush()

O ile sprawdzanie czy dane przyszły można robić w ciągłej pętli to wysyłać polecenie odczytu stanu potencjometru nie powinniśmy robić ciągle. Dlatego kod decydujący o tym czy wysłać kolejne polecenie odczytu wygląda tak:

        if (time() - sent_time > 0.1 or response):
            read_analog(radio)
            sent_time = time()
            response = False

W zmiennej sent_time przechowujemy czas kiedy został nadany ostatni raz pakiet z poleceniem odczytu potencjometru. Jeżeli minie więcej niż 0.1 sekundy lub już otrzymaliśmy odpowiedź to wysyłamy polecenie do tinyBrd. Wysłanie polecenia oznaczmy przez zmianę czasu zapisanego w sent_time oraz kasując wartość response.

Cały szkic dla tinyBrd oraz skrypt dla Raspberry są do pobrania: