Choinka

Christmas Tree App

Witajcie wszyscy odwiedzający mój blog. Od ostatniego wpisu upłynął ponad rok. W tym czasie w projekcie Stacji Pogodowych nie wiele się zmieniło. Drobne aktualizacje, które nieco usprawniają działanie całego systemu, ale o tym w innym artykule.

Tutaj chciałbym się skupić na moim nowym projekcie. Powstał on na prośbę mojej żony, która w zeszłym roku na Boże Narodzenie znalazła filmik, w którym to pokazana jest aplikacja w telefonie umożliwiająca kolorowanie światełek na choince w dowolny sposób. Zażyczyła sobie takiego rozwiązania na przyszłe Święta (te w 2021). Stwierdziłem, że to niezły chelenge i warto podjąć wyzwanie. Zgłębiłem nieco temat na początku. Znalazłem kilka gotowych rozwiązań, niestety nie należały one do najtańszych, stąd decyzja żeby przygotować swoje rozwiązanie.

Po rozważaniach czego użyć, co mam, a co zakupić zapadła decyzja aby projekt został oparty o 12V diody RGB w postaci łańcucha światełek zakupione na Allegro. Do tego leżące na półce Nodemcu, przetwornica step-down i zasilacz 12V 100W (gdyż światełek były aż 4 komplety połączone szeregowo). Suma wydatków całego zestawu była niższa o jakieś 30% – 40% od gotowego rozwiązania, ale całe serce systemu to oprogramowanie, które należało napisać.

Założenia

Jak już wspomniałem podstawowe założenie systemu to możliwość kolorowania choinki. Ja od siebie dorzuciłem jeszcze kilka rzeczy:

  • nadawanie efektów
  • możliwość uruchomienia na telefonie
  • możliwość zdalnej aktualizacji aplikacji
  • zapisywanie pewnych informacji o konkretnych ustawieniach (gdzieś w jakiejś pamięci, najlepiej na urządzeniu)

I to właściwie tyle, albo aż tyle jak na pierwsze MVP.

Jak zbudować aplikację?

Jeśli chodzi o napisanie aplikacji to podejście było dość oczywiste.

Mikrokontroler

Nodemcu postanowiłem oprogramować przy pomocy Arduino IDE i C++. Niestety dość szybko się okazało, w odróżnieniu od wcześniejszych projektów, iż ten interfejs IDE jest mało komfortowy przy tak dużym projekcie i dość szybko się przesiadłem na Visual Studio Code. Uff… można w końcu programować „po ludzku”.

Ta część aplikacji miała odpowiadać za zapalanie LEDów na odpowiednie kolory i przyjmowanie różne requesty na wystawiony API przy pomocy prostego serwera WWW.

Aplikacja Desktop/Mobile

Druga część aplikacji odpowiadać miała za interfejs użytkownika. Jak wiecie z moich wcześniejszych wpisów jestem fanem Angulara, więc wybór był oczywisty. Chciałem za jednym zamachem zbudować aplikację na desktop i na mobile (z użyciem NativeScript). Niestety ten plan się nie powiódł. Zabrakło czasu i możliwości. Zatem zostałem przy jednej aplikacji WWW. Powstał problem gdzie ją umieścić, tak by wszyscy mogli z niej korzystać na różnych urządzeniach. Początkowo myślałem o wystawieniu dockera (i tak aplikacja była rozwijana), potem jednak padł pomysł a może uda się zmieścić apke razem z kodem źródłowym na Nodemcu (tak powstała finalna wersja).

Wyzwania

Skoro wiemy już jak wygląda z grubsza architektura czas na krótkie podsumowanie największych wyzwań.

Odzwierciedlenie lampek w aplikacji

Najtrudniejsze w całym przedsięwzięciu było zadanie odzwierciedlenia układu lampek na choince w płaskim widoku drzewka na ekranie telefonu czy komputera i jak potem to wszystko przełożyć na bibliotekę Adafruit Neopixel, która traktuje światełka jako listę uszeregowaną.

Po rożnych rozważaniach i konsultacjach udało się wypracować następujące rozwiązanie.

Wychodzimy od następującego założenia, iż światełka zaczynamy zakładać od dołu choinki zgodnie z ruchem wskazówek zegara. Po zamontowaniu dokonujemy następującej konfiguracji. Zapalamy kolejno lampki od dołu choinki i zapamiętujemy numery światełek dla których zataczamy kolejne kręgi, w ten sposób uzyskamy podział choinki na okręgi. Każdy okrąg oczywiście nie jest domknięty gdyż światełka pną się do góry i tworzą swego rodzaju spiralę. Ten pomysł można zobrazować jeszcze nieco inaczej. między pierwszą lampką na dole a ostatnią u góry tworzę linię. Teraz zapalając po kolei lampki od dołu ilekroć przekroczę moją wirtualną linię to powinienem odnotować, iż stworzyłem kolejny okrąg.

Split tree to segments
Podział choinki na segmenty

Dla uproszczenia te okręgi będę nazywał przedziałami. Teraz zakładając, że światełka są w miarę równo rozłożone, to dzielę każdy przedział na dwie części i powstaje mi taka oto choinka z zaznaczonymi lampkami (może mało udany zrzut ekranu, ale każde koło reprezentuje lampkę).

Lights on the tree
Rozłożenie lampek na choince

Jak widać teraz, należy tylko odczytać wszystkie ledy po kolei od dołu i tak przygotowany zestaw kolorów wysłać do aplikacji na mikrokontrolerze, która to zapali odpowiednio LEDy.

Komunikacja ESP8266 <-> UI

Komunikacja między UI a ESP8266 mogłaby się odbywać przynajmniej na kilka rodzai, ja jednak zdecydowałem się na najprostszy czyli RESTowe API, wykorzystujące JSONa jako sposób przesyłania danych. Wybór ten wydawał się oczywisty i prosty w swej konstrukcji ze względu na to, że zarówno ESP8266 jak i Angular radzą sobie z tym formatem danych. W trakcie prac zostały przygotowane odpowiednie struktury danych do przesłania i przygotowane testy na jednej nitce ledów (50 sztuk). W tym wydaniu system zachowywał się dość poprawnie i działał zarówno zapis jak i odczyt takich ustawień. Niestety problem pojawił się tuż na końcu projektu kiedy to został przeskalowany, tzn. zostały podłączone 4 komplety ledów, czyli 200 lampek. Wówczas to okazało się, że dane 200 lampek są za duże i server nie jest w stanie tego przetworzyć. Obiekt ArduinoJson przekraczał dopuszczalny rozmiar w pamięci i program się wysypywał. W tym przypadku wystarczyło zamienić listę typu:

[
  {
    "index": 0,
    "color": {
      "r": 255,
      "g": 255,
      "b": 255,
    }
  },
  ...,
  {
    "index": 199,
    "color": {
      "r": 255,
      "g": 255,
      "b": 255,
    }
  },
]

na

[
  {
    "i": 0,
    "c": "#FFFFFF"
  },
  ...,
  {
    "i": 199,
    "c": "#FFFFFF"
  },
]

i od razu system zaczął działać lepiej.

Następnie okazało się, że wielokrotne przetwarzanie różnych requestów tworzy w pamięci kolejne obiekty ArduinoJsoni i ich nie niszczy przez co powoduje wyciek pamięci i zawieszenie aplikacji. W tym przypadku wystarczyło stworzyć jeden globalny obiekt do przetwarzania wszystkich JSON’ów, którego zadaniem było parsowanie i przygotowywanie requestów i odpowiedzi. Oczywiście tutaj należy dodać że zakładamy pewną synchroniczność zdarzeń, ale o tym w następnym akapicie.

Synchroniczność vs Asynchroniczność

Generalnie w przypadku Arduino jak i ESP8266 mamy do czynienia z nieskończoną pętlą, która odpala nasz program w kółko. Tak też rozpocząłem prace nad swoją „choinką”. Dodałem bibliotekę odpowiedzialną za wystawienie serwera WWW, zrobiłem odpowiednie handlery i po jakimś czasie choinkę dało radę pokolorować, zapisać bieżący zestaw kolorów, wczytać zestaw itd. niestety wraz z upływem czasu zachciało mi się dorzucić efekty do już pokolorowanej choinki i tu na pierwszy ogień wymyśliłem dwa:

  • default – statyczny kolor
  • blink – migające z pewną prędkością światełka

I w tym miejscu pojawił się pierwszy problem, jak ogarnąć temat tego, że z jednej strony choinka ma efekt „default”, czyli świeci w normalny sposób, ja wykonuję zapytanie o zmianę efektu na „blink” i ta zmiana następuje. Oczywiście, zmiana nastąpiła, ale ze wzgłedu na to że „blink” jest nieskończoną pętlą to zapytanie HTTP się nie skończyło. Do UI wrócił ServerError, a ja już nie mogłem wykonać żadnego innego zapytania HTTP. Hmm… jak to obejść? Czy jest to możliwe? Po chwili namysłu stwierdziłem , że musi być możliwe, bo przecież słynne WLED działają w podobny sposób. Zacząłem nieco zgłębiać tajniki i okazało się, że jest biblioteczka, która nazywa się ESPAsyncWebServer i pozwala na stworzenie asynchronicznego serwera WWW. Wow pomyślałem to jest to czego mi trzeba. Jak się szybko okazało było to idealne rozwiązanie, jednak nie udało mi się tak szybko przerobić moich handlerów na nowe rozwiązanie. Główny problem jaki się pojawił to sposób przetwarzania Payloadu, który z pojedynczego przetworzenia (pobrania całego „body” i przetworzenia) zamienił się na kilka przetworzeń („body” został podzielone na kilka chainów i przetworzone). Takie podejście spowodowało problemy z przetwarzaniem zapytań z dużą ilością przesyłania danych, ale i to udało się zwalczyć, choć nie było łatwe (szczegóły w późniejszych artykułach, jak opublikuję kod na GitHub).

Jak serwować UI?

Jak pisałem wcześniej, wstępnie myślałem o wystawieniu dockera, ewentualnie zrobieniu plugina do Home Assisatnt, ale każde z tych rozwiązań wymagało użycia kolejnego pośrednika między telefonem/desktopem a ESP, co mogło powodować różne komplikacje ciężkie do debugowania. Zastanawiałem się czy mając serwer WWW nie mogę udostępnić statycznych plików, no i oczywiście można, pozostawało tylko rozwiązać problem wgrywania skompilowanych plików JavaScript na ESP. Jak się okazuje to też nie jest trudne i z pomocą Platform.io oraz Visual Studio Code, wszystko poszło dość sprawnie. Miałem co chciałem, UI serwowany wprost z urządzenia, kolorowanie, efekty itp. Pozostał jeszcze tylko jeden problem – aktualizacja.

Aktualizacja

Aktualizowanie oprogramowania było dość proste gdy całość była podłączoną przez USB do komputera, ale co zrobić, gdy cały zestaw zostanie złożony, osadzony na choince i ukryty tak by nie był przez nikogo widoczny. Jak wtedy wgrać poprawkę, dodać nowy efekt czy funkcjonalność? Tutaj rozwiązanie przyszło samo, dosłownie „Out of the box”. Cudowna biblioteka AsyncElegantOTA, integruje się bezproblemowo z AsyncWebServer’em i daje gotowe rozwiązanie, który działa jak za dotknięciem magicznej różdżki. W prosty sposób pozwala zaktualizować kod oprogramowania czy system plików (statyczne pliki JS z nowym UI). Nie myślałem nawet, że będzie to takie proste. Dzięki twórcom za tak wspaniałą bibliotekę.

Czy było warto?

Wielu z Was zapewne zapyta „czy było warto?”. Zdecydowanie odpowiem „TAK” i spróbuję to uzasadnić:

  • ekonomia – projekt na pewno wyszedł mnie nieco taniej niż zakup gotowego rozwiązania, myślę że zaoszczędziłem jakieś 300zł, poświęciłem jednak mnóstwo czasu
  • nauka – postawiłem sobie wysokie wymagania, czy udało się sprostać – myślę, że tak; udało się zrobić MVP, a przy tym nauczyłem się sporo o optymalizacji, o tym jak działa ESP, jak programować urządzenia z ograniczoną ilością pamięci Flash i przede wszystkim RAM, to naprawdę duże doświadczenie, gdyż na co dzień nie mam takich wyzwań, no może nie aż na taką skalę, by walczyć o każdy bajt
  • zabawa – to chyba najważniejsze, cieszyła mnie każda działająca linijka kodu, gdy LEDy robiły to co chciałem to cieszyłem się niemal jak dziecko ze swojego pierwszego zestawu klocków LEGO

Co dalej?

Dalej… teraz potrzebuję chwilę wypoczynku i przemyśleń. Usystematyzowania tego co mam, optymalizacji i upublicznienia kodu. Później w planach mam nowe efekty i przede wszystkim stworzenia aplikacji UI natywnie dla Androida, czy się uda? Czas pokaże, mam nadzieję, że podeślecie mi jakieś pomysły, a może ktoś będzie chciał dorzucić jakiś swój kod, efekt albo nowy feature. Jestem otwarty na wasze propozycje, komentarze.

Choinka
Przewiń na górę