Heltec Wireless Stick v3 – mapowanie zasięgu LoRaWAN (TTN) – część 3

Teraz pora na jakieś „prawdziwe” eksperymenty z siecią TTN. Na wstępie przypomnienie – sieć TTN może być używana za darmo, ale pamietaj o ograniczeniach – do 30 s łącznego czasu nadawania na 24h. Nie flooduj sieci, nawet jeżeli masz jeszcze limit dobowy do wykorzystania.

Będziemy próbowali utworzyć mapę rzeczywistego zasięgu sieci TTN. Podczas takich prób będzie cię kusiło wysyłać często wyniki pomiarów aby uzyskać dokładniejszy obraz zasięgu. Nawet jeżeli będziesz miał sporo „niewykorzstanego” limitu czasu nadawania, serwer sieci TTN może zacząć ignorować twoje wiadmości jeśli je zbyt często wysyłasz.

By zmapować zasięg nasz Heltec musi wiedzieć gdzie się znajduje. Najprościej użyć modułu GPS. Z różnych projektów w szufladzie mam kilka VK2828U7G5LF. Do tego typu zadań wolę je niż NEO6MV2, nie tylko dlatego, że mam je pod ręką. Antena i moduł są w jednym kawałku. Lubię to, bo gdy budujesz takie jednorazowe układy to masz jeden element do zamocowania zamiast dwóch. Do naszych celów mapowania sieci nadadzą się dobrze.

Chyba każdy projekt DIY używający GPS, oparty o środowisko Arduino korzysta z biblioteki TinyGPS. Nie będziemy inni, podłączamy GPS do Serial1 na ESP32 – jeśli nie muszę nie korzystam z SoftwareSerial, znacznie mniej problemów. Ponieważ nic nie będziemy konfigurować w module GPS wystarczą trzy przewody – GND, zasilanie i TX z modułu do RX Serial1 (GPIO18 w ESP32-S3). Akurat VK2828U7G5LF ustępuje tutaj NEO jako, że nie ma złącza w rastrze 2.54. Ale jeszcze przy wcześniejszych projektach przygotowałem kabel – ten będący w zestawie przylutowałem do drabinki goldpin. Zlutowaną stronę potraktowałem klejem na gorąco, owinąłem kawałkiem kartki papieru i mam gotową taką uniwersalną przejściówkę na raster 2.54 – już mogę użyć zwykłych przewodów F-F do połączenia GPS z Wireless Stick Lite.

Skoro mam już podłączony GPS, obsługa w programie sprowadza się do odczytania każdego bajtu przychodzącego na Serial1 i wysłanie go do obiektu GPS. Gdy ten będzie miał informacje z GPS, że pozycja już jest określona będzie można to sprawdzić przez gps.location.isValid().

    while (Serial1.available() > 0) {
        gps.encode(Serial1.read());
    }

Ten powyższy kawałek kodu w loop pozwoli by biblioteka TinyGPS zajęła się przetwarzaniem wszystkich danych z modułu GPS.

Cayenne LPP

Mamy współrzędne, jak wysłać je do TTN? W części 1 pisałem że najlepiej używać formatu binarnego (najmniej bajtów). Można pisać własne, ale ktoś to robił przed nami, w świecie IoT bardzo popularny jest Cayenne LPP, którego oczywiście użyjemy.

Cayenne LPP jest binarnym formatem danych, przystosowanym do przesyłania wielu różnych typów danych. Repozytorium na GitHub zawiera dokumentację – szukaj części dotyczącej LoRa. Jako że Cayenne jest popularnym formatem danych w świecie Arduino znajdziesz sporo bibliotek do jego obsługi. Ja wybrałem CayenneLPP od ElectronicCats.

W tej implementacji każda wiadomość może przesyłać różne wartości (ich zestaw nie musi być identyczny w każdej wiadomości). Czyli w jednej wiadmości możesz wysłać wartość temperatury i wilgotności a w kolejnej temperatury, wilgotności i ciśnienia atmosferycznego. W CayenneLPP rozróżniamy wartości przez nadanie im numeru kanału. Jaki numer im nadasz to twoja sprawa byle użycie było konsekwentne. Jak wybrałeś kanał 1 na temperaturę a kanał 2 na wilgotność to trzymaj sie tego we wszystkich wiadomościach.

Obsługa formatu odbywa się przez obiekt u nas nazwany lpp

CayenneLPP lpp(15);

15 podane w konstruktorze jest rozmiarem bufora na wiadomość. My będziemy wysyłac tylko lokalizację, która w CayenneLPP zajmuje 9 bajtów (listę typów danych i ile zajmują bajtów znajdziesz w linkowanej wcześniej dokumantacji Cayenne LPP), więc 15 bajtów jest aż nadto.

Jak sie domyślasz zmiana nastąpi w funkcji send_lora_frame. Sprawdzimy czy moduł GPS już podaje nam lokalizację i jeśli tak przygotujemy wiadomość w formacie Cayenne LPP:

    lpp.reset();
    if( gps.location.isValid()) {
        lpp.addGPS(1, gps.location.lat(), gps.location.lng(), gps.altitude.meters());
    } else return;
    if (lpp.getSize() > LORAWAN_APP_DATA_BUFF_SIZE) {
        Serial.println("Too big LPP buffer!!!, not sending!");
        return;
    }
    memcpy(m_lora_app_data.buffer, lpp.getBuffer(), lpp.getSize());
    m_lora_app_data.buffsize = lpp.getSize();

Najpierw czyścimy bufor przez lpp.reset . Jeśli lokalizacja jest dostępna(gps.location.isValid()) wtedy dodajemy lokalizację do bufora przez addGPS. Pierwszy argument (1) to numer kanału, następnie długość i szerokość geograficzna oraz wysokość na poziomem morza. Tutaj zdajemy się w zupełności na moduł GPS i bibliotekę TinyGPS, która to karmiona w loop robi wszystkie wyliczenia.

Jeśli nie ma dostępnych danych, wtedy nie wysyłamy danych, po co pchać pustą wiadomość. Teraz opisując całość mnie tknęło, że w zasadzie wypadało by sprawdzić nie tylko czy isValid() ale też age() na wypadek gdy moduł stracił widoczność satelitów i nie ma aktualnej pozycji (ale ma „prawidłową” starą, bo pobieżnym przejrzeniu kodu TinyGPS nie widzę „unieważniania” lokalizacji ze względu na czas, który upłynął od ostatniej komunikatu z modułu GPS).

Wszystkie typy danych, które są obsługiwane przez implementację Cayenne LPP spod rąk Electronic Cats znajdziesz tutaj. Każda funkcja addXXXX zwraca indeks wskazujący na koniec danych w buforze albo 0 – jeśli nie udało się dodać danych do bufora utworzonego razem z obiektem lpp. Szczerze mówiąc wypadałoby sprawdzić czy addGPS nie zwraca 0, ale ponieważ to kod tylko z jednym parametrem wiemy że zmieści się w 15 bajtach bufora. Ale taka przestroga – zdarzało mi się taki „jednorazowy” kod używać jako baza do czegoś większego i wtedy taki zapomniany „check” może się zemścić.

Następny krok – sprawdzamy czy pakiet przygotowany przez LPP zmieści się w buforze wysyłania utworzonym przez bibliotekę. Znowu, wydaje się takie sprawdzenie zbędne bo LORWAN_APP_DATA_BUFFER_SIZE zdefiniowane jest na 64 bajty, ale warto wyrabiać sobie nawyk sprawdzania zawsze rozmiarów buforów/tablic. Unikniecie wielu problemów w przyszłości…

Jesteśmy teraz gotowi skopiować przygotowaną wiadomość do bufora LoRa: memcpy(m_lora_app_data.buffer, lpp.getBuffer(), lpp.getSize());

Dane są gotowe do wysłania, jednak pozostaje jeszcze jedna rzecz. W przypadku badania zasięgu wysyłanie danych w ustalonym interwale nie ma specjalnego sensu. Może i byłoby sensowne gdyby można wysłać dużą ilość pakietów. Gdy jesteśmy mocno ograniczeni przez Fair use policy (FUP) sensowniejsze jest wysyłanie danych na żądanie, po naciśnięciu przycisku. Obsługa przycisku wydaje się trywialna, ale są pewne niuanse i dla uproszczenia sprawy skorzystam z biblioteki Bounce2 zamiast pisać od zera:

Bounce2::Button button = Bounce2::Button();
void setup()
{
    //Button
    button.attach(4, INPUT_PULLDOWN);
    button.interval(5);
    button.setPressedState(HIGH);
}

Wybrałem GPIO4 (bez specjalnego powodu) jako to obsługujące przycisk. Przycisk podpięty drugim wyprowadzeniem do 3.3V. Port GPIO4 został podłączony do masy przez wewnętrzny rezystor (pull-down). Jeśli procesor nie obsługuje takiego rozwiązania (są dziś takie?) to zawsze musisz podpiąć przez rezystor wejście albo do zasilania albo do masy, tak by w czasie gdy przycisk nie jest załączony na wejściu była ustalona wartość napięcia (zasilające lub masa). Te ustawienia (GPIO4 i włączony pull-down) „załatwia” polecenie attach z powyższego kodu. Funkcja interval ustawia limit czasu na 5 milisekund. Przez ten czas biblioteka będzie ignorować kolejne zmiany stanu przycisku po wykryciu pierwszej zmiany. To zapobiega wystąpienia zjawiska „bouncing-u”, czyli widmowym naciśnięciom przycisku na skutek mechanicznych drgań styków. Pisałem o tym tutaj na blogu, na samym jego początku. Na marginesie – nazwa biblioteki do tego zjawiska właśnie nawiązuje. setPressedState informuje bibliotekę, która wartość (LOW czy HIGH) jest uznawana za stan wciśniętego przycisku.

Jedyne co jest konieczne do poprawnej obsługi przycisku jest periodyczne wywoływanie button.update() np w loop. Po spełnieniu tego warunku możemy korzystać z funkcji biblioteki:

    if (button.pressed()) {
        if (millis()-lastSend > 10000) {
            send_lora_frame();
            lastSend = millis();
        } else {
            Serial.println("Too frequent uplink! Wait a bit longer...");
        }
    }

lastSend to zmienna na przechowanie czasu nadania ostatniego pakietu. Ustawiłem tutaj 10 s (10000 milisekund) aby przypadkowo nie wysłać dużej ilości uplinków. 10 s i tak zbyt często, ale przynajmniej przypadkowo nie będę floodował.

Teraz pozostaje przygotować konsolę do przyjmowania wiadomości w formacie Cayenne LPP. Jak pamiętacie poprzednio napisaliśmy własny formater danych. Teraz skorzystamy z gotowca. Wejdź na konsolę i wybierz nowy formater dla danych w uplinku.

Wyślij pakiet z lokalizacją:

Nazwę urządzenia usunąłem

Jeśli przejdziesz we właściwości urządzenia teraz znana jest jego lokalizacja – TTN wie jakie dane wysłane zostały i rozumie że to lokalizacja.

TTN Mapper

W pierwszej części odsyłałem Cię do strony TTN Mapper byś mógł sprawdzić czy jesteś w zasięgu sieci. Teraz poznamy ostatni element sieci LoRaWAN, który czyni ją niesamowicie użyteczną. Intergracje – czyli możliwość przekazywania danych do zewnętrznych aplikacji. Skoro mamy wiadmość z lokalizacją urządzenia, wystarczy utworzyć integrację z TTN Mapper by tą informację przekazać. TTN Mapper oprócz naszej wiadomości otrzyma inne parametry związane z odebranym pakietem danych – będzie znał też siłę sygnału. W ten sposób właśnie powstaje mapa którą widzisz na TTN Mapper.

By stworzyć integrację wybierz Integrations w konsoli i wybierz Webhooks. Kliknij Add webhook i wyszukaj kafelk z TTN Mapper. Po jego wyborze pojawia się okno konfiguracji integracji:

Webhook id i twój email są niezbędne. Jeśli wprowadzisz nazwę eksperymentu to tej nazwy będziesz mógł używać by potem filtrować dane na mapie w TTN Mapper.

Utwórz integrację i możesz ruszać w teren! Znowu przypomnienie – NIE FLOODUJ sieci TTN!

Dane na mapie są dostępne niemal na żywo. Jak zobaczyć dane? Idź na stronę TTN Mapper i wybierz Advanced maps. Podaj device id, możesz ograniczyć dane do określonych dni i View map!

No i jak?

Przykładowa mapka z moimi testami. Czerwony – sygnał najsilniejszy, ciemno niebieski – najsłabszy.

Kilka słów o rezultatach. Bramka LoRaWAN zamontowana jest na dwuspadowym dachu domu jednorodzinnego, na wysokości, lub tuż poniżej kalenicy. Kalenica jest zorientowana N-S a bramka jest po zachodniej stronie. Wydaje się że kalenica ma wpływ na rozkład siły sygnału – gdy urządzenie jest na wschód od bramki sygnał jest nieco słabszy, bo kalenica stanowi przeszkodę.

Generalnie, zwłaszcza na dalszym dystansie kluczowa jest potencjalna możliwość bezpośredniego zobaczenia bramki przez urządzenie (LOS – Line Of Sight). Dlatego wzdłuż ulicy ciągnącej się na wschód jest możliwa łączność – ale wystarczy skręcić w boczną uliczkę, tak że domy stanowią przeszkodę i już pakiet nie dociera do bramki.

Temat sprawdzania łączności i zasięgu mnie interesuje i będę robił więcej testów, ale zapewniam was, że jest to zdanie wymagające dużo czasu, więc nie wiem kiedy tutaj się pojawi coś więcej na ten temat.

Niemniej jednak od razu pojawia się pytanie, co można zrobić by poprawić zasięg? Są dwie ścieżki. Pierwsza, dość oczywista to użycie lepszej anteny. Powyższa mapka to rezultat testów z standardową, maleńką anteną typu „sprężynka„, dołączoną w zestawie z Wireless Stick Lite. Zmieńmy ją na lepszą, mającą uzysk 5 dBi na 868 MHz oraz dla wygody testowania 50 cm pigtail SMA. Wyniki?

Najlepiej porównać to na animowanym gifie (otwórz obraz jeśli twoja przeglądarka nie animuje go domyślnie).

Kliknij by zobaczyć animację

Bez dokładnego mierzenia – maksymalny zasięg jest niemal 2 x większy.

No dobra a druga ścieżka?

Wszystkie pomiary zostały dokonane na domyślnych ustawieniach biblioteki. Jednak LoRa oferuje dużo więcej, ale jak zwykle – nie ma nic za darmo. Zainteresowanych nieco fizykaliami działania LoRa odsyłam do tego bardzo interesującego artykułu na Medium, objaśniających jak działa transmisja w LoRa. Dane w LoRa są reprezentowane przez zmieniająca się częstotliwość w czasie. Im bardziej transmisja jednostki danych jest rozciągnięta w czasie, tym lepszy zasięg można osiągnąć. Teraz już chyba rozumiecie czemu ten parametr transmisji nazywa się Spreading Factor (SF)?

Jaka praktyczna różnica w czasie transmisji występuje? Domyślnie biblioteka LoRaWAN używa SF9 i z takim ustawieniem robiłem wszystkie testy. Jeśli w konsoli TTN zajrzysz w szczegóły odebranego pakietu znajdziesz tam parametr consumed_airtime. On właśnie powie ci ile czasu zajęło nadanie paczki danych. Wiadomość z naszego przykładu zawierająca tylko pozycję GPS (9 bajtów) nadawana była przez 0.2s. Po przejściu na SF12 (maksymalnie rozciągnięty sygnał) czas nadawania wydłużył się do 1.45 s – czyli 7.25 razy dłużej . Proste obliczenie: przy SF9 w 30 s dobowego okna zgodnego z FUP nasz maksymalnie 30/0.2 = 150 wiadomości. 150/24 = 6.25 – czyli dane możesz wysłać raz na 10 minut. Zmiana na SF12 oznacza, że możesz nadać 30/1.45 = 20 wiadomości – czyli nawet nie jedną raz na godzinę! A to tylko 9 bajtów, wysłanie kliku parametrów może szybko zwiększyć liczbę bajtów a co za tym idzie czas nadawania. I budżet 30 s przy wysokim SF okaże się maleńki.

Każdy wybór ma konsekwencje. Oczywiście, jestem ciekaw na ile SF12 (czy może nieco niższy SF ale nadal większy niż SF9) poprawi użyteczny zasięg. Jak zbiorę więcej danych to się podzielę i porównamy osiągnięte zasięgi na tym samy urządzeniu nadawczym z tą samą bramką.

No i na koniec – kod!

Całość przykładu z tego artykułu, tradycyjnie w formie projektu PlatformIO do pobrania tutaj.