Poniższy artykuł przedstawia wewnętrzny projekt, realizowany przez inżynierów-programistów z Centrum Kompetencyjnego Embedded firmy Sii.
Od strony technicznej projekt pokazuje rozwiązanie uniwersalnego koncentratora (HUBa) zbierającego dane z wszelkiej maści sensorów czym wpisuje się jednoznacznie w tematykę Internet of Things. Z drugiej strony tekst skupia się na całym procesie myślowym jaki doprowadził do realizacji technicznej – a więc od pomysłu, przez projekt po implementację oraz zapewnienie jakości.
Jak do tego doszło?
Idea projektów wewnętrznych narodziła się dawno temu wraz z samym Centrum Kompetencyjnym Embedded. Skoro bowiem ponad setka inżynierów jest w stanie realizować projekty dla szerokiego grona klientów, to dlaczego by nie połączyć ich umiejętności programistycznych, pracy w grupie wraz z dobrymi praktykami projektowania oprogramowania. To wszystko okrasić elementami Agile’a i mamy gotowy przepis na sukces oraz ciekawy projekt zarazem.
Szybko znalazła się w firmie grupka pasjonatów gotowa połączyć swe siły pod szyldem wspomnianego projektu wewnętrznego.
Bezpośrednim celem było zaprojektowanie i wykonanie kompletnego systemu/produktu z jednoczesnym poszerzeniem i pogłębieniem wiedzy z dziedziny Internet of Things oraz spięcie w ramach jednego projektu możliwie dużej liczby technologii ze wspomnianego już IoT. Jednocześnie chcieliśmy po prostu zrobić coś ciekawego od A do Z i mieć z tego czysto inżynierską przyjemność.
Okazało się jednak, że sam wybór tematu projektu – części wydawałoby się prozaiczna (w porównaniu oczywiście do późniejszej realizacji) stanowi pierwszą kość niezgody. Padały przeróżne pomysły związane z technologiami bezprzewodowymi, kryptografią, domeną automotive, czy nawet sztuczną inteligencją. Do kompletu chmura i element technologii webowych… aż chciałoby się zapytać czy może jeszcze frytki do tego.
W końcu jeden z kolegów przypomniał, że już od dawna dyskutowaliśmy na forum wewnętrznym o zrobieniu uniwersalnego koncentratora do wszelkiej maści urządzeń z domeny IoT. Może warto więc efekty wcześniejszych dyskusji przekuć w czyn? Tak oto dobrnęliśmy do końca początku – znaleźliśmy temat projektu: Thálassa, Thálassa![1]
This is madness
Pierwotnym pomysłem było zastosowanie wspomnianego koncentratora (nazwanego na potrzeby projektu IoT HUBem) do celów typu home automation. Ale zaraz, zaraz. Przecież istnieje szereg gotowych rozwiązań – tutaj każdy zaznajomiony z tematem automatyki domowej wymieni jednym tchem takie rozwiązania open source jak Domoticz[2], OpenHAB[3], czy Home Assistant[4]. Prawda, za to zespół postawił sobie za cel:
- opracowanie WŁASNEGO, uniwersalnego koncentratora (IoT HUBa) umożliwiającego komunikację z całą gamą urządzeń końcowych
- przygotowanie prostego, otwartego protokołu komunikacyjnego
- integrację z możliwie największą ilością urządzeń końcowych
Urządzenie to zostanie następnie wyprodukowane w ilości kilku egzemplarzy i umieszczone w różnych oddziałach Sii.
Od teorii do praktyki, czyli projekt rozwiązania
Najtrudniej jest zacząć (aka przypadki użycia)
Kiedy już zespół chciał ochoczo i z kopyta wziąć się do wyboru platformy oraz pisania kodu, postanowiłem zadać kluczowe pytanie – „Czy aby na pewno wiecie co mamy do opracowania?”. W pierwszym odruchu wszyscy przytaknęli nieśmiało, ale gdy każdy opisał słowami swoją wizję produktu, to wyszły na jaw znaczne rozbieżności co do ogólnej koncepcji, użytej platformy i systemu operacyjnego.
Zaproponowałem więc odłożenie dyskusji nad technicznymi szczegółami na później oraz zastosowanie w tym projekcie zasad, jakimi kierujemy się w projektach komercyjnych. Odrzucając najlepsze praktyki z początków takich gigantów jak Apple, Google czy Microsoft (a wszystkie one powstały w przydomowych garażach swoich właścicieli) postanowiliśmy nie robić przysłowiowej garażówy i zacząć od wyspecyfikowania abstrakcyjnych przypadków użycia.
Zakładając, że nasz IoT HUB będzie czarną skrzynką, każdy z nas zadał sobie pytanie – „Jak i do czego chciałbym używać wspomnianego urządzenia?”. W efekcie powstało 12 przypadków użycia w formie graficznej, każdy poparty bardziej szczegółowym opisem słowno-muzycznym.
Wymagania i ogólna koncepcja urządzenia
W następnym kroku poddaliśmy pod dyskusję ogólną koncepcję sprzętową urządzenia biorąc pod uwagę dwie możliwości:
- all-in-one z wbudowanymi w IoT HUBa modułami komunikacyjnymi
- rozszerzalną: z płytą główną i doczepianymi expanderami/gateway’ami
Ostatecznie zdecydowaliśmy się na wersję rozszerzalną, zaś koronnym argumentem była możliwość zbudowania takiej konfiguracji przy użyciu ogólnodostępnych zestawów developerskich.
Uzgodniwszy ogólny schemat sprzętowy, przystąpiliśmy do przekucia przypadków użycia w proste, jasne i krótkie wymagania. Tym sposobem powstała lista kilkudziesięciu wymagań, które wraz z przypadkami użycia zamknięte zostały w LaTexowym dokumencie.
Wybór platformy i systemu operacyjnego
Mając zdefiniowaną listę wymagań oraz uzgodniony ogólny schemat części sprzętowej przystąpiliśmy do wyboru platformy i systemu operacyjnego. Zespół podzielił się na dwie, równoliczne grupy: zwolenników:
- rozwiązań opartych o Linuxa
- rozwiązań opartych o systemy operacyjne czasu rzeczywistego
Dyskusjom, argumentom i kontr-argumentom nie było końca. Ostatecznie zwaśnione strony pogodziła koncepcja rozwiązania (o zgrozo) uniwersalnego, mogącego pracować pod kontrolą
- dowolnego systemu operacyjnego
- szerokiego spektrum platform sprzętowych: głównie 32 bitowych
Struktura oprogramowania
Mając na uwadze wspomnianą wcześniej uniwersalność podzieliliśmy oprogramowanie na 4 zasadnicze bloki:
- warstwę abstrakcji od sprzętu (HAL), wystawiającą quasi-CMSISowe[5] API
- warstwę services, wystawiającą za pomocą dedykowanego API bloki funkcji do aplikacji oraz systemu operacyjnego
- warstwę aplikacji, wykorzystującą zarówno API od services jak i funkcje wystawione przez system operacyjny
- complex driver[6], będący ukłonem w stronę świata automotive i zawierający te moduły, które nie dadzą się wpiąć/przyporządkować do żadnej z powyższych warstw.
Na poniższym rysunku widzimy poszczególne, opisane powyżej bloki funkcjonalne. Wszelkie interfejsy z modułów zaznaczone są kolorami.
Osobną kwestią stało się opakowanie systemu operacyjnego w taki sposób, aby łatwo dało się go podmienić na inny. Stąd dedykowane API wzorowane na POSIXie.
Do kompletu powstał jeszcze długi (i nudny) opis API wystawianego przez każdą z w/w warstw.
Specyfikacja bloków funkcjonalnych
Kolejnym przed-implementacyjnym etapem projektowania była specyfikacja bloków funkcjonalnych na warstwie aplikacji naszego IoT HUBa. Uzgodniliśmy z całą pewnością, że potrzebujemy bloków odpowiedzialnych za:
- ogólne zarządzanie urządzeniem, co spadło na barki System Managera
System Manager jest najwazniejszym „bytem”. Jego zadaniem jest inicjalizacja wszystkich podległych mu modułów funkcjonalnych, obieranie od nich eventów informujących o zdarzeniach oraz wyzwalanie akcji w odpowiedzi na nie.
- zarządzanie urządzeniami końcowymi (sensorami i efektorami)
W zamyśle blok ten odpowiada za dodawanie/usuwanie/aktualizowanie stanu urządzeń końcowych. Zarówno bezprzewodowych, jak i przewodowych.
- zarządzanie expanderami
Podobnie jak to ma miejsce w przypadku urządzeń końcowcych istnieje możliwość wykrywania/dodawania/usuwania expanderów (modułów rozszerzeń) za pośrednictwem dedykowanego bloku.
- obsługę akcji
Użytkownik IoT HUBa ma możliwość definiowania zdarzeń/akcji w stylu „jeżeli nastąpiło X to wykonaj Y”. Manager akcji ma za zadanie umożliwić dodawanie/usuwanie/wykonywanie wspomnianych zdarzeń w reakcji na inne zdarzenie.
- bazę danych
Służącą do zapisu konfiguracji oraz wszelkich potrzebnych systemowi danych nieulotnych (np. logów)
- zarządzanie poborem energii i stanem baterii
IoT HUB pozwala na pracę zarówno przy zasilaniu z sieci (domyślne) jak i z baterii. Stąd istnieje konieczność racjonalizacji zużycia energii oraz generowania akcji gdy np. napięcie baterii spadnie poniżej pewnego poziomu
- komunikację
Wszelka komunikacja przewodowa/bezprzewodowa/szeregowa/równoległa jest zarządzana z poziomu tego bloku.
Wspomniane bloki funkcjonalne należało podzielić między poszczególne wątki – uzgodniliśmy w końcu, że korzystamy z dobrodziejstw systemu operacyjnego. Sztywne podejście z jednym wątkiem na każdy blok funkcjonalny wydało nam się zbytnią rozrzutnością. Stąd zdecydowaliśmy się na 5 współpracujących ze sobą wątków o 3 różnych priorytetach.
Ostatnim elementem architektonicznej układanki jest sposób komunikacji między wątkami. Każdy wątek dysponuje pojedyncza kolejką przez którą przekazywane są wiadomości w ustandaryzowanym formacie. Wiadomość zawiera m.in.:
- identyfikator wątku nadawczego (wartość liczbowa)
- identyfikator wiadomości, jest to wartość odpowiadająca konkretnemu zdarzeniu/żądaniu
- wskaźnik na payload (opcjonalny)
- wielkość payloadu w bajtach (wartość liczbowa)
Realizacja, czyli wcielamy słowo w czyn
Dość już teorii, przejdźmy do opisu realizacji projektu.
Zespół projektowy
Zespół projektowy był, jest i będzie płynny oraz z założenia rozproszony lokalizacyjnie. Składają się na niego osoby pracujące ze wszystkich biur firmy Sii i dokładające swoją cegiełkę w ramach posiadanych umiejętności/doświadczenia/opanowanego zakresu technologii jak również posiadanego czasu.
Zarządzanie zadaniami
Wszystkie zadania projektowe zarówno te wynikające z pierwszej analizy wymagań jak i dodane w czasie późniejszym są trzymane w trackerze Jira w formie user stories i wynikających z nich zadań.
W ramach zwinnego podejścia do wytwarzania oprogramowania zespół na bieżąco (w 2 tygodniowych cyklach) analizuje, estymuje, wdraża i testuje kolejne funkcjonalności systemu.
Przegląd kodu
W ramach wspólnej kolaboracji nad kodem, wdrożyliśmy prosty proces przeglądu i oceny kodu oparty o GitHuba. W ramach niego każda zmiana, wykonana na odpowiednim branchu i wystawiona w formie pull-requesta jest oceniania przez co najmniej dwóch członków zespołu pod kątem:
- Poprawności implementacji względem dokumentacji/projektu
- Poprawności implementacji pod kątem programistycznym
- Istnienia testów pokrywających nową funkcjonalność
Struktura projektu
Kod źródłowy podzieliliśmy na części odpowiedzialne za:
- Zależną od sprzętu część zawierającą niskopoziomowe sterowniki (zaznaczone na zielono)
- Aplikację (zaznaczoną na zielono) implementującą opisane wcześniej moduły funkcjonalne
- Warstwę pośrednią (services) dostarczającą niezależne od sprzętu funkcjonalności (zaznaczone na pomarańczowo)
Do tego kilka katalogów zawierających pliki nagłówkowe od:
- POSIX API oddzielającego system operacyjny od aplikacji
- API warstwy services (pośredniej)
- CMSIS API warstwy abstrakcji od sprzętu
Kod źródłowy i podejście do programowania
Sam kod źródłowy napisaliśmy w języku C. Poniżej nieco przydługi, ale jednocześnie treściwy fragment obrazujący proces inicjalizacji systemu, jego wątków i kolejek.
Wszystkie funkcjonalności dostarczane przez system operacyjny zostały opakowane – widzimy je w postaci makr i funkcji z przedrostkiem OS.
Identyczne podejście zastosowaliśmy przy implementacji poszczególnych wątków. Wewnątrz umieszczonej w wątku pętli nieskończonej następuje:
- Cykliczne aktualizowanie statusu (jak w przypadku zamieszonego poniżej wątku systemowego)
- Oczekiwanie na pojawienie się wiadomości w przypisanej kolejce
Wybrane rozwiązania
Konsola debugowa
Zgodnie z wymaganiami istnieje możliwość diagnozowania i podglądania stanu IoT HUBa. W tym celu zaimplementowaliśmy konsolę debugową wraz z prostym menu.
Z jednej strony kluczowe dla działania systemu informacje są „wyrzucane” na port szeregowy – wystarczy umieścić w kodzie wywołanie stosownego makra. Jednocześnie istnieje też możliwość wysłania prostego, znakowego zapytania (jest to pojedynczy znak ASCII) i otrzymania np. aktualnego IP urządzenia.
Kolejnym etapem będzie implementacja logu w pamięci nieulotnej wraz z możliwością jego odczytywania i czyszczenia z poziomu powyższej konsoli.
Serwer WWW
Aby usprawnić i przyspieszyć konfigurację IoT HUBa oraz umożliwić zdalny dostęp do jego zasobów, postawiliśmy na nim serwer WWW, oparty o LwIP. Sama integracja LwIP do projektu poszła sprawnie – wspieraliśmy się przy tym oprogramowaniem do generowania kodu, dostarczonym przez ST (CubeMX).
Na poniższym przykładzie widoczne jest okno służące do konfigurowania i wizualizowania funkcji termostatu – o czym opowiem szerzej w części opisującej przygotowane demo.
Manager scen
Obecnie trwają pracę nad dodatkiem pozwalającym na dodawanie/usuwanie/monitorowanie akcji (nazwanym dalej scenami). W tym celu zaprzęgnięty został silnik graficzny oparty o googlowe Blockly.[7]
Za jego pomocą można w prosty sposób zmontować konkretną scenę i zapisać ją w pamięci IoT HUBa w formie pliku JSON. Następnie scena jest wykonywana, przy zajściu zdefiniowanych w niej warunków, przez wbudowany w IoT HUBa interpreter kodu Java Script.
Jenkins, testowanie – jednym słowem jakość
Żaden projekt, do którego kontrybuuje wiele osób, nie może się obyć bez zapewnienia choćby minimum testowania/jakości.
Do tego kwestia:
- automatyzacji wykonywanych testów i generowania raportów testowych,
- tworzenia paczek releasowych,
- statycznej analizy kodu
również powinna zostać zaadresowana i zaimplementowana w projekcie.
Google test (Gtest)
Testy niskopoziomowych driverówZdecydowaliśmy się na wdrożenie w projekcie biblioteki unit testowej od Google’a. Wymagało to napisania sporej liczby mocków. Przy czym same testy podzieliliśmy w sposób identyczny jak samo oprogramowanie:
- Testy warstwy pośredniej (services)
- Testy warstwy aplikacyjnej
Sam framework testowy daje dużo możliwości jeżeli chodzi o raportowanie – za jego pomocą możemy wygenerować szczegółowy raport dla każdego z plików źródłowych z konkretnym podziałem pokrycia testami na:
- Linie kodu źródłowego
- Rozgałęzienia w kodzie (branche)
- Konkretne funkcje
Statyczna analiza kodu (CppCheck)
Dodatkowo postanowiliśmy narzucić sobie jeszcze jeden element – narzędzie do statycznej analizy kodu, celem wyeliminowania potencjalnych i realnych luk w kodzie źródłowym oraz zabezpieczenia się przed wszelkimi programistycznymi gafami, które umknęły uwadze recenzentów.
Znowu rozgorzały jednak dyskusje na temat wyboru konkretnego narzędzia. Ostatecznie podjęliśmy decyzję o użyciu CppChecka i traktowaniu generowanych przez niego wyników w charakterze wskazówek.
Testy automatyczne
Wymienione wyżej elementy zostały spięte pod sztandarem Jenkinsa, zainstalowanego na dedykowanej maszynie testowej. Dzięki temu w ciągu kilku minut otrzymujemy w pełni automatyczny i obiektywny status na temat jakości oprogramowania IoT HUBa.
Podsumowanie
Projekt IoT HUB pozwolił nam znacząco poszerzyć i ugruntować kompetencje z dziedziny Internet of Things. W ramach realizacji powstał, w dalszym ciągu rozwijany i udoskonalany, produkt łączący w sobie wiele technologii.
W toku prac dotychczasowych rozwiązaliśmy sporą ilość inżynierskich i typowo organizacyjnych problemów, takich jak na przykład:
- Szybkie wdrażanie nowych osób, z różnych lokalizacji, do projektu
W tej kwestii pomogło opracowanie kompleksowej instrukcji, przeprowadzającej krok po kroku przez proces zestawiania środowiska oraz wdrażającej w arkana projektu
- Nieoczekiwany reset urządzenia spowodowany pojawieniem się wyjątku
Tutaj pomogło wyposażenie exception handlera w dedykowany fragment kodu, służący do odszukania adresu w pamięci programu, z którego nastąpiło wygenerowanie wyjątku. Zwykle problem był spowodowany wyciekiem pamięci lub nadpisaniem stosu wątku przez inny wątek.
- Brakiem przestrzeni dyskowej na maszynie Jenkinsowej
Na wczesnym etapie projektu wszystkie artefakty budowania były niepotrzebnie archiwizowane po stronie Jenkinsa. Gdy ilość danych przekroczyła 300 GB zaczęły pojawiać się błędy spowodowane niemożnością zapisu kolejnych plików. Wyczyszczenie przestrzeni roboczej Jenkinsa połączone z ograniczeniem ilości archiwizowanych artefaktów do ostatnich 30 buildów rozwiązało problem.
Patrząc na projekt od strony czysto technicznej wykorzystaliśmy następujące technologie i narzędzia:
- Od strony programistycznej
- C
- C++
- Java Script
- Groovy
- Od strony komunikacyjnej
- Bluetooth Low Energy
- LoRa
- UART, I2C, SPI
- LwIP
- Narzędzia
- Eclipse
- CubeMX,
- GitHub
- Jira
- Zapewnienie jakości
- Jenkins
- GTest
- CppCheck
.223rem
[1] Za Historią Grecką Ksenofronta
[4] https://www.home-assistant.io/
[5] CMSIS – Cortex Microcontroller Software Interface Standard
[6] Complex driver jest pojęciem ze standardu AUTOSAR i określa moduł/moduły, które nie są zdefiniowane przez ten standard. Głównym celem complex driver’a jest zamknięcie w nim złożonych, niestandardowych czy odziedziczonych sterowników (legacy drivers).
Zostaw komentarz