wSołtysiak BlogPraktycznie o tworzeniu oprogramowania i nie tylko
18 sierpnia 2023

TinyGS — prosty rotor do obserwacji satelitów LEO

Po ponad pół roku od budowy własnej stacji TinyGS, postanowiłem ulepszyć ją o rotor. W tym wpisie przejdziemy przez napisanie własnego API do określania najbliższego przelotu satelity, budowę nowej antetny, zaprogramowanie płytki ESP32 do sterowania serwem oraz złożeniem tego wszystkiego w całość.

Zbliżenie na chałupniczno wykonany rotor

Jeżeli chcesz dowiedzieć się więcej na temat TinyGS, to zapraszam Cię na oficjalną stronę projektu lub do mojego pierwszego wpisu, gdzie przedstawiam budowę stacji od podstaw.

Założenia projektowe

Zanim przejdziemy do konkretów, przedstawię założenia, które przyjąłem w ramach tego projektu. Część z nich pozwoliła na znaczne uproszczenie prac. Oto one:

  • Rotor musi wskazywać na satelitę, który jest aktualnie nasłuchiwany przez TinyGS. Jest to dość oczywiste, ale wprowadza trochę zamieszana. System działający na płytce, która odbiera sygnał, nie wie nic o rotorze. Mechanizm obsługujący rotor także nie może skomunikować się z płytką znajdującą się zaledwie kilka centymetrów dalej. Nie istnieje też prosta możliwość podłączenia wszystkiego pod jedną płytkę. Z tego powodu będziemy musieli odwzorować algorytm dobierania nasłuchiwanej satelity.
  • Rotor musi działać na akumulatorkach. Spowodowane jest to brakiem wyprowadzonego zasilania na mój balkon.
  • Rotor musi być odporny na złe warunki pogodowe. Stacja znajduje się na dworze, wiec jest narażona na mocny wiatr, deszcz oraz śnieg.
  • Wystarczające jest, żeby rotor obracał się wyłącznie po płaszczyźnie horyzontu. Podnoszenie anteny na konkretną elewację komplikuje budowę stacji oraz wymaga dodatkowego serwa/silniczka. Posiłkując się ciekawym wpisem, uznałem to za zbędne w przypadku satelitów znajdujących się na niskiej orbicie okołoziemskiej. Statystycznie okazuje się bowiem, że ustawienie anteny pod kątem 15 stopni pozwala na osiąganie bardzo dobrych rezultatów.
  • Rotor nie musi na bieżąco śledzić satelity. Wystarczy, że ustawi się w optymalnym miejscu jeden raz na przelot satelity. Ciągłe namierzanie pozycji satelity komplikuje kod oraz znacząco skraca czas działania na baterii. Biorąc pod uwagę fakt, że statycznie osadzona antena Moxon odbierała sygnały z dość szerokiego zakresu, uznałem, że wystarczy wycelować antenę w punkt, gdzie satelita będzie najbliżej stacji.

Całość składa się z API, które zwraca dane o najbliższym przelocie satelity oraz oprogramowania umieszczonego na płytce ESP32, które pobiera dane z API i steruje serwem z przymocowaną anteną.

Lista zakupowa

Podobnie jak w poprzednim wpisie, umieszczam kompletną listę potrzebnych części, narzędzi i innych materiałów, które są niezbędne do zbudowania stacji z rotorem. Ceny są aktualne na dzień pisania artykułu.

  • płytka LilyGo TTGO LoRa32 (433 MHz) — 87 zł
  • najprostsza płytka ESP32 — 30 zł
  • antena

    • kabel instalacyjny — 10 zł
    • gniazdo SMA kwadratowe montażowe — 12 zł
    • 2x przedłużka SMA — 28 zł
    • listewka drewniana — 4 zł
    • plastikowy klin pod drzwi — 8 zł
    • przeciwwaga (4 nakrętki + mały kątownik) — 4 zł
  • rotor

    • serwo Feetech FS5106B — 44 zł
    • mocowanie do serw Pololu 3435 — 16 zł
    • korek kanalizacyjny 160 mm — 19 zł
  • zasilanie

    • 4x akumulatory Li-Ion 18650 3400 mAh — 100 zł
    • ładowarka do akumulatorów Li-Ion 18650 — 30 zł
    • 2x koszyk na akumulator 18650 — 4 zł
    • 8x akumulatorów NiMH AA (R6) 2500 mAh — 100 zł
    • ładowarka do akumulatorów NiMH AA (R6) — 50 zł
    • koszyk na baterie cztery baterie AA (R6) — 4 zł
  • 2x puszka natynkowa 80x80x40 mm (minimum IP44) — 8 zł
  • maszt

    • długa kantówka strugana — 20 zł
    • 4x wspornik stalowy wzmocniony 400 mm — 60 zł
  • narzędzia i pozostałe materiały

    • stacja lutownicza ZD8906 — 85 zł (chociaż widziałem ostatnio podobne stacje w Lidlu za 60 zł, które też mają dobre opinie)
    • cyna do lutowania — 30 zł
    • trzecia ręka — 16 zł
    • pistolet do klejenia 60W + 2 wkłady — 39 zł
    • śrubki, wkręty — 4 zł
    • kompas — 12 zł

Całkowity koszt: 824 zł

API

Realizacje projektu rozpocząłem od wykonania API. Jest ono dość skromne, ponieważ składa się wyłącznie z jednego endpointu. Przyjmuje on współrzędne geograficzne naszej stacji wraz z wysokością, na której się znajduje. Jako odpowiedź otrzymujemy dane o najbliższym przelocie satelity w zasięgu naszej stacji:

  • nazwa satelity
  • azymut [°]
  • elewacja (wartość wystawiona na przyszłość) [°]
  • pozostały czas do wejścia satelity w zasięg stacji [ms]
  • czas, w którym satelita jest w zasięgu stacji [ms]

Przykładowo dla żądania /look-angles/51.781/16.677/0.085/ otrzymamy odpowiedź:

{
  name: "GaoFen-7",
  azimuth: 288,
  elevation: 12,
  millisecondsToPass: 60000,
  passDuration: 360000
}

Wykorzystane technologie

Chcąc przy okazji trochę poeksperymentować z czymś nowym, zdecydowałem się na użycie Deno oraz oak. W celu obliczania konkretnej pozycji satelity skorzystałem z biblioteki satellite-js, którą podpatrzyłem na stronie TinyGS.

Przy tak małym projekcie ciężko ocenić mi potencjał takiego stacku technologicznego. Jeżeli wcześniej ktoś używał Express.js, to nie powinien mieć żadnego problemu ze wdrożeniem się do biblioteki oak. Deno wydaje mi się dobrym narzędziem do tworzenia prototypów. Żeby zacząć pisać w TypeScript nie trzeba niczego konfigurować. Siadamy do edytora kodu i piszemy — za to duży plus.

API hostuję na Deno Deploy. Nie zgłębiałem się szczególnie w możliwości tej platformy, mogę tylko napisać, że postawienie aplikacji jest proste i co najważniejsze — za darmo. Na duży minus brak integracji z GitLabem, przez co musiałem specjalnie wrzucić kod na GitHuba. Początkowo API wystawiałem na Fly.io, ale z niewiadomego powodu po kilku dniach dostawałem same timeouty. W przyszłości może przejdę na self-hosting albo ponownie spróbuję sił z Fly.io.

Ustalanie najbliższego przelotu satelity

Do działania naszego API potrzebujemy listę dostępnych satelitów wraz z ich TLE. TinyGS trzyma takie dane w pliku TXT. Lista ta jest aktualizowana na bieżąco, więc przy każdym zapytaniu API będzie pobierana na nowo. Można dorobić cachowanie, ale przy tej skali uznałem to za zbędne.

Po pobraniu i odpowiednim dostosowaniu danych z pliku warto usunąć dane o satelitach, których stacja nie jest w stanie odebrać. W moim przypadku są to satelity ThingSat oraz Gossamer, ponieważ nadają one na innej częstotliwości. W tym celu zastosowałem prostą blacklistę, która jest ustalona na sztywno w kodzie. Jej uzupełnianie będzie zdarzać się sporadycznie, więc nie jest to dużym problemem.

Mając TLE satelitów oraz współrzędne naszej stacji możemy za pomocą funkcji dostępnych w satellite-js obliczyć, na jakim azymucie będziemy mogli obserwować satelitę w danym momencie. Nam jednak potrzebna jest informacja o tym, jak satelita będzie się poruszać w dłuższym okresie. Żeby ustalić najbliższy przelot satelity w zasięgu stacji zastosowałem następujący algorytm:

Dane wejściowe:

  • tle — TLE satelity
  • groundStation — dane geograficzne stacji
  • now — aktualny czas
  • minuteShift — 0 minut
  • lookAngles — pusta tablica

Kroki:

  1. Oblicz pozycję satelity na podstawie tle oraz groundStation dla czasu now + minuteShift i umieść na końcu tablicy lookAngles
  2. Jeżeli satelita była poprzednio nad horyzontem, a teraz jest poniżej horyzontu (co oznacza koniec przelotu) lub minuteShift == 1801:

    • Przejdź do kolejnego kroku
    • W przeciwnym przypadku: minuteShift += 1 oraz powróć do kroku pierwszego
  3. Określ czas, w którym satelita jest w zasięgu stacji przez zliczenie elementów lookAngles, gdzie elevation > 0 i zapisz do passDuration

    • Jeżeli passDuration == 0, zakończ działanie
  4. Określ czas, jaki satelita potrzebuje do znalezienia się w zasięgu przez zliczenie elementów, gdzie eleveation <= 0 i zapisz do millisecondsToPass
  5. Znajdź element z największą wartością elewacji2 i zapisz ten element do bestLookAngle
  6. Zwróć dane passDuration, millisecondsToPass, bestLookAngle

Mając do dyspozycji dane o przelotach wszystkich satelitów w ciągu najbliższych trzech godzin, wystarczy posortować je rosnąco po polu millisecondsToPass i zwrócić pierwszy element. W ten sposób otrzymamy dane najbliższego przelotu satelity w zasięgu naszej stacji.

Czasami zdarzają się sytuacje, kiedy w zasięgu stacji pojawia się kilka satelitów równocześnie. W tym przypadku wybieram satelitę, który jest najbliżej stacji (pojawia się na najwyższej na horyzoncie).

Zabezpieczanie API

Oprócz walidacji parametrów stacji trzeba pochylić się jeszcze nad problemem zależności od pliku tinygs_supported.txt znajdującego się na zewnętrznym serwerze. Może okazać się, że zasób będzie tymczasowo niedostępny lub wystąpią problemy sieciowe. Każde żądanie po ten plik ma ustawiony timeout na 3 sekudny. W Deno możemy użyć zwykłego fetch, skorzystałem więc z bardzo przydanej opcji o nazwie signal. Przekazujemy tam instancję AbortSignal, dzięki czemu po upływie okreslonego czasu żądanie zostanie przerwane:

const res = await fetch(
  'https://api.tinygs.com/v1/tinygs_supported.txt',
  {
    signal: AbortSignal.timeout(3000)
  },
);

Następnie wystarczy obsłużyć wyjątek i zwrócić odpowiedni kod HTTP z API. W tym przypadku będzie to 504 Gateway Timeout.

Uruchomienie WiFi i pobranie danych to najbardziej pochłaniająca energię czynność na płytce ESP32. Brak scenariusza na wystąpienie błędu po stronie API to prosta droga do wydrenowania baterii w bardzo krótkim czasie. Z tego powodu, jeżeli sterownik rotora otrzyma błąd z API, to przechodzi w stan uśpienia na 15 minut i następnie ponawia próbę pobrania danych.

Kod źródłowy do opisanego wyżej API znajdziesz tutaj.

Rotor

Drugą część projektu rozpoczniemy od wybrania oraz zbudowania odpowiedniej anteny. Następnie ustawimy serwo oraz zasilimy całą elektronikę. Na końcu zaprogramujemy naszą płytkę ESP32.

Antena

Mając do dyspozycji rotor, możemy postawić na bardziej ukierunkowaną antenę. Przeglądając kanał TinyGS na Telegramie natknąłem się na dyskusję na temat anteny Yagi LFA i postanowiłem dać jej szansę. Poniżej zamieszczam schemat tej anteny w wariancie dla 435 MHz:

Schemat wykorzystanej anteny Yagi LFA dla 435 MHz

Sama konstrukcja jest bardzo prosta. Do jej wykonania użyłem miedzi z kabla instalacyjnego oraz gniazda SMA kwadratowego montażowego. Wszystko połączyłem na drewnianej listwie za pomocą kleju na gorąco.

Zgodnie z założeniami projektu, antena powinna być ustawiona pod kątem 15° względem horyzontu. W tym celu użyłem klina pod drzwi, który przykleiłem od spodu listewki.

Ostatnim elementem jest przeciwwaga. O tym niestety nie pomyślałem za pierwszym razem i musiałem improwizować po złożeniu całej stacji. Okazało się, że antena jest na tyle ciężka, że lekko się przechyla, niwelując w ten sposób kąt uzyskany przez klin. Dokleiłem więc po przeciwnej stronie malutki kątownik razem z czteroma dużymi nakrętkami.

Zdjęcie wykonanej antenty

Pamiętajmy, że antena będzie się obracać, więc połączenie kablem/przedłużkami SMA pomiędzy płytką i anteną powinno mieć lekki zapas.

Serwo

Jako element obracający wybrałem serwo typu standard Feetech FS5106B. Zastanawiałem się także nad silniczkiem krokowym, ale wyższa cena i większy stopień skomplikowania sterowaniem spowodowały, że postawiłem na serwo.

W przypadku ustawienia stacji na balkonie zakres 180° powinien być wystarczający. Wybrane serwo z łatwością radzi sobie z obracaniem lekkiej anteny, więc można zaryzykować i użyć także serwo o słabszych parametrach.

Do przymocowania serwa użyłem specjalnego mocowania do serw Pololu 3435. Nie było to może najtańsze rozwiązanie, ale wszystkie inne sposoby, które próbowałem albo nie pasowały do mojej koncepcji, albo kończyły się brakiem stabilności.

Jako podstawę pod antenę oraz jednocześnie zabezpieczenie serwa przed deszczem użyłem zwykłego korka kanalizacyjnego. Może wygląda to trochę dziwnie, ale spełnia swoje zadanie.

Przymocowane serwo

Istotną kwestią jest ustalenie zakresu pracy rotora. W tym celu należy wybrać punkt na horyzoncie, od którego rotor będzie zaczynał swoją pracę i za pomocą kompasu określić jego azymut. Będzie to minimalny obsługiwany azymut. Żeby określić maksymalny azymut, wystarczy dodać liczbę stopni obsługiwaną przez nasze serwo.

Jeżeli wybraliśmy serwo obracające się o 360°, dla uproszczenia obliczeń, wystarczy wyznaczyć północ i tak ustawić serwo, żeby rotor rozpoczynał swoją pracę od tego miejsca.

Zasilanie

Do zasilenia mamy trzy elementy: dwie płytki ESP32 oraz serwo. Wszystkie elementy stacji będą zasilane osobno dla uproszczenia projektu. Minusem tego rozwiązania jest większa liczba akumulatorków i ich dość częsta wymiana.

Zasialanie, płytki i całe okablowanie

Wybrane przeze mnie serwo wymaga od 4.8V do 6V, więc nie możemy go zasilić bezpośrednio z płytki ESP32. W tym celu potrzeba koszyczka z czteroma akumulatorkami AA. Przy podłączaniu pamiętajmy, że należy połączyć uziemienie akumulatorków z pinem GND na płytce ESP32. Akumulatorki o pojemności 2500 mAh wystarczają na ponad tydzień działania rotora.

Schemat połączenia płytki ESP32 i serwa

Do płytek ESP32 podłączymy akumulatorki 18650 o pojemności 3400 mAh. W przypadku płytki obsługującej oprogramowanie TinyGS, która cały czas jest aktywna, starcza to na ok. 33 godziny działania. Dla płytki sterującej rotorem czas działania na baterii to około dwa tygodnie.

Programowanie ESP32 w MicroPython

Mając do dyspozycji wcześniej napisane API, możemy zaprogramować płytkę ESP32. Nie przepadam za C++, więc postanowiłem sprawdzić, jak wygląda praca z MicroPythonem i muszę powiedzieć, że jestem więcej niż zadowolony. Wykonanie prostego programu do obsługi serwa było bardzo przyjemnym doświadczeniem. Wszystko, co potrzebne (np. łączenie się z siecią WiFi, pobieranie danych z API) to dosłownie kilka wierszy kodu.

Jako IDE wybrałem banalny w obsłudze program Thonny, dzięki któremu bez problemu wgramy napisany przez nas program na płytkę. Do obsługi serwa posiłkowałem się micropython-servo.

Program działa według następującego schematu:

  1. Połącz się z siecią WiFi
  2. Pobierz dane z API

    • Jeżeli API zwróciło błąd, przejdź w tryb deep sleep na 15 minut
  3. Sprawdź, czy azymut satelity jest w zakresie działania serwa

    • Jeżeli pozycja satelity jest w zakresie działania serwa, przekaż dalej ten azymut
    • Jeżeli pozycja satelity jest poza zakresem działania serwa, przekaż dalej najbliższy (skrajny) obsługiwany azymut
  4. Obróć rotor na podany azymut3
  5. Przejdź w tryb deep sleep na milisecondsToPass + passDuration

Po wybudzeniu urządzenia program uruchamiany jest od nowa.

Podsumowanie

Po kilku tygodniach działania jestem bardzo zadowolony z wyników. Na mapie ze zaznaczonymi pakietami widać wyraźnie, że stacja pokrywa cały dostępny horyzont.

Nowa antena przy dobrych warunkach odbiera pakiety dosłownie z samego horyzontu. Odległości pomiędzy satelitą a stacją wynoszące powyżej 2300km nie robią już większego wrażenia. Odebranie pakietów Sattla-2B, satelity nadającego z najmniejszą mocą, z odległości ponad 2000km mówi samo za siebie.

Plan na przyszłość to zejście z kilku zastosowanych tutaj uproszczeń i rozpoczęcie prac nad ciągłym śledzeniem satelity podczas jej przelotu. Platforma TinyGS daje także możliwość ręcznego wybierania nasłuchiwanych satelitów, co otwiera drogę do integracji rotora z aktualnie wybraną satelitą.

Sama konstrukcja masztu także wymaga poprawek. Podczas burzy mam czasami wrażenie, że niektóre części zaraz się oderwą i rozpoczną podróż w nieznane.

W razie pytań zapraszam do kontaktu przez link na końcu strony.

Źródło schematu anteny Yagi LFA


  1. Wartość dobrana na podstawie obserwacji. Nie powinna wystąpić sytuacja, gdzie przez taki czas w zasięgu stacji nie pojawi się żaden satelita. Ma to za zadanie ograniczyć obliczenia oraz równocześnie zapobiec sytuacji, że program nie zwróci danych kolejnego przelotu.

  2. Założyłem, że ustawienie anteny w kierunku miejsca, gdzie satelita będzie najbliżej, da największe szanse jej złapanie. Oczywiście nie zawsze się to sprawdza. Dla przykładu, jeżeli satelita leci prawie idealnie nad stacją, to antena będzie najprawdopodobniej ustawiona prostopadle do lotu satelity, co przekreśla szanse na odebranie większości pakietów danych z tego przelotu.

  3. Stopnie obrotu serwa nie będą odpowiadać azymutowi satelity. Jeżeli pozycją początkową serwa nie jest północ i należy to odpowiednio przeliczyć.