Jeszcze niedawno sterowanie oświetleniem choinkowym ograniczało się do prostych sekwencji realizowanych za pomocą układów logicznych i timerów, na przykład NE555. Popularność ekosystemu Arduino i ogromna łatwość w budowaniu oprogramowania i układów z nim związanych, sprawiła, że skonstruowanie prostego systemu do sterowania lampkami choinkowymi nie jest zbyt dużym wyzwaniem, jak na okazjonalny, świąteczny projekt. Oprócz mikrokontrolerów, także same źródła światła lampek choinkowych zrewolucjonizowały takie projekty. Moduły diod elektroluminescencyjnych RGB, które w swojej strukturze integrują, oprócz trzech źródeł światła – czerwonego, zielonego i niebieskiego, także kontroler cyfrowy, pozwalają łatwo programować wynikowy kolor świecenia modułu. Moduły RGB LED, takie jak WS2811 pozwalają w prosty sposób wykonać dowolne oświetlenie – dzięki interfejsowi szeregowemu, mogą być łatwo kontrolowane przez mikrokontroler.
Ostatnim zaawansowanym elementem, który pozwala na tworzenie zadziwiających systemów sterowania są usługi w chmurze. W tym projekcie skorzystano z ekosystemu Amazon Web Services, który w prosty sposób można zintegrować z asystentem głosowym Amazon Alexa. Dzięki temu można sterować choinką (czy dowolnym systemem) za pomocą prostych komend głosowych. Opisany projekt wykorzystuje asystenta Amazon Alexa, do sterowania łańcuchami diod RGB, zawieszonymi na choince. Dalsza część artykułu omawia komponenty sprzętowe, zawierające moduł Arduino, oraz oprogramowanie, podzielona na dwie części – firmware, pracujące na module Arduino oraz oprogramowanie pracujące w chmurze.
Komponenty sprzętowe
Aby zbudować własny system zarządzania światełkami choinkowymi, sterowany przez Alexę potrzebne następujące komponenty:
- moduł Arduino Yun z zainstalowanym systemem Linino OS,
- dwie taśmy LED WS2811 z 50 diodami LED każda,
- trzy komplety przewodów połączeniowych,
- zasilacze ze złączem DC 2,1×5,5 mm,
- kabel z złączami Micro-USB i USB,
- asystent głosowy – Amazon Echo, Amazon Dot lub Amazon Tap.
Budowa systemu
Konstrukcja systemu jest bardzo prosta i nie wymaga zbyt wielu połączeń. Zasadniczo Wystarczy podłączyć moduł Arduino do sieci i zasilania oraz podpiąć listwy LED do wyjścia z modułu Arduino. Oba łańcuchy diod połączone są ze sobą szeregowo i są sterowane z pinu szóstego modułu Arduino. Jeśli wybierzemy inny pin, należy zmienić to w listingu Arduino (listing 1).
#include <Adafruit_NeoPixel.h>
#include <aws_iot_mqtt.h>
#include <aws_iot_version.h>
#include "aws_iot_config.h"
#define PIN 6 // Pin do którego podłączone są diody
#define NUM_LEDS 100 // Liczba diod
#define BRIGHTNESS 100 // Maksymalna jasność diod
// Inicjalizacja sterownika NeoPixel dla diod RGB LED
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, PIN, NEO_RGB + NEO_KHZ800);
// Inicjalizacja klienta MQTT
aws_iot_mqtt_client myClient;
char msg[32]; // bufor dla odczytu i zapisu
int cnt = 0; // licznik pętli
int numYieldFailed = 0;
int rc = -100; // zmienna wartości zwrotnej
bool success_connect = false; // status połączenia
char JSON_buf[100];
void setup() {
Serial.begin(115200);
strip.setBrightness(BRIGHTNESS);
strip.begin();
strip.show();
// Początkowy kolor ustawiany na pomarańczowy
changeColorBackwards(160, 52, 3, 30);
strip.show();
char curr_version[80];
snprintf_P(curr_version, 80, PSTR("AWS IoT SDK Version(dev) %d.%d.%d-%s\n"),
VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, VERSION_TAG);
Serial.println(curr_version);
while(success_connect == false) {
connect();
}
}
void connect() {
// Dodaje losowy numer do ID, aby nie konfliktować z poprzednim połączeniem
Serial.println("Try connect with client-id: " + String(AWS_IOT_CLIENT_ID));
if((rc = myClient.setup(AWS_IOT_CLIENT_ID, true, MQTTv311, true)) == 0) {
if((rc = myClient.configWss(AWS_IOT_MQTT_HOST, AWS_IOT_MQTT_PORT, AWS_IOT_ROOT_CA_PATH)) == 0) {
if((rc = myClient.connect()) == 0) {
success_connect = true;
light_status_led(0, 255, 0, 1000);
print_log("shadow init", myClient.shadow_init(AWS_IOT_MY_THING_NAME));
print_log("register thing shadow delta function",
myClient.shadow_register_delta_func(AWS_IOT_MY_THING_NAME, msg_callback_delta));
}
else {
light_status_led(255, 0, 0, 330);
delay(330);
light_status_led(255, 0, 0, 330);
Serial.println(F("Connect failed!"));
Serial.println(rc);
}
}
else {
light_status_led(255, 0, 0, 200);
delay(200);
light_status_led(255, 0, 0, 200);
delay(200);
light_status_led(255, 0, 0, 200);
Serial.println(F("Config failed!"));
Serial.println(rc);
}
}
else {
light_status_led(255, 0, 0, 1000);
Serial.println(F("Setup failed!"));
Serial.println(rc);
}
// Opóźnienie, dodane aby odebrać SUBACK od serwera.
// Rzeczywiste opóźnienie zależne jest od serwera
delay(2000);
}
void loop() {
if(success_connect) {
if(myClient.yield()) {
light_status_led(124, 114, 32, 500); // Dioda statusowa na żółto
Serial.println(F("Yield failed."));
if (numYieldFailed++ > 9) { // Ponowne połączenie po 10 próbach
reconnect();
}
}
else {
light_status_led(0, 0, 255, 500);
numYieldFailed = 0; // resetuje licznik błędów
}
delay(500);
}
}
void msg_callback_delta(char* src, unsigned int len, Message_status_t flag) {
Serial.println(F("Message arrived."));
if(flag == STATUS_NORMAL) {
print_log("getDeltaKeyValue", myClient.getDeltaValueByKey(src,
"io.klerch.alexa.xmastree.skill.model.TreeState\"r", JSON_buf, 50));
int r = (String(JSON_buf)).toInt();
Serial.println(r);
print_log("getDeltaKeyValue", myClient.getDeltaValueByKey(src,
"io.klerch.alexa.xmastree.skill.model.TreeState\"g", JSON_buf, 50));
int g = (String(JSON_buf)).toInt();
Serial.println(g);
print_log("getDeltaKeyValue", myClient.getDeltaValueByKey(src,
"io.klerch.alexa.xmastree.skill.model.TreeState\"b", JSON_buf, 50));
int b = (String(JSON_buf)).toInt();
Serial.println(b);
print_log("getDeltaKeyValue", myClient.getDeltaValueByKey(src,
"io.klerch.alexa.xmastree.skill.model.TreeState\"mode", JSON_buf, 50));
String mode = String(JSON_buf);
Serial.println(mode);
if (mode == "COLOR") {
changeColor(r, g, b, 300);
}
else if (mode == "ON") {
changeColor(r, g, b, 300);
}
else if (mode == "OFF") {
changeColorBackwards(0, 0, 0, 50);
}
else if (mode == "SHOW") {
long showTime = 0;
while(showTime < 50000) {
int to = random(5, strip.numPixels());
showTime = showTime + (10 * to);
changeColorPartial(random(0, 255), random(0, 255), random(0, 255), 10, to);
Serial.println(showTime);
int to2 = random(5, strip.numPixels());
showTime = showTime + (10 * to2);
changeColorBackwardsPartial(random(0, 255), random(0, 255), random(0, 255), 10, to2);
Serial.println(showTime);
}
Serial.println(showTime);
changeColor(r, g, b, 15);
}
else if (mode == "STOP") {
Serial.println("Would stop show because SHOW.");
changeColor(r, g, b, 0);
}
else {
Serial.println("Unexpected mode given – do nothing.");
}
}
}
Moduły RGB WS2811 pozwalają na szeregowe łączenie modułów jeden za drugim. Wynika to z faktu, że dioda RGB sterowana jest za pomocą bardzo prostego, jednopinowego interfejsu szeregowego, który ma dodatkową logikę i wyjście szeregowe, które można podłączyć do wejścia kolejnego modułu. Zasada działania kontrolera jest bardzo prosta. Przyjmuje szeregowo dane i zatrzaskuje pierwsze 24 otrzymane bity (po 8 bitów na każdy składowy kolor), a dalsze dane są po prostu przekazywane na wyjście układu. W ten sposób można połączyć ze sobą nawet 1024 moduły RGB. Kontroler wbudowany w module posiada trzy generatory przebiegu PWM z driverami diod LED o składowych kolorach. Wypełnienie sygnałów sterujących poszczególnych diod sterowane jest za pomocą danych zatrzaśniętych w module – dla każdej z diod po osiem bitów.
Oprogramowanie
System sterowania choinką może wydawać się zaskakująco złożony. Na rysunku 1 pokazano schemat zależności poszczególnych elementów. Rozwiązanie wykorzystuje szereg usług chmurowych AWS do komunikacji z zapleczem sprzętowym – choinką. Jedyne, co trzeba samodzielnie skonfigurować w chmurze Amazona, to funkcja Lambda, S3 bucket (w uproszczeniu – to rodzaj dysku chmurowego) zawierający pliki MP3 i role IAM z uprawnieniami dla AWS IoT i Dynamo. Tabela w Dynamo, a także „cień rzeczy” w AWS IoT zostaną utworzone automatycznie podczas pierwszego wywołania systemu.
Przyjrzyjmy się wysokopoziomowej architekturze systemu oraz prześledźmy, co dzieje się po otrzymaniu żądania głosowego, przekazanego do urządzenia Alexa przez użytkownika:
- Użytkownik rozmawia z Alexą i wydaje mu komendę „open the christmas tree”. Magia rozpoznawania mowy z udziałem algorytmów neuronowych, dzieje się po stronie usługi chmurowej Alexa.
- Uruchamiany jest kod, znajdujący się w AWS Lambda.
- Jeśli użytkownik chce tylko wykonać akcję, taką jak „turn on the tree”, aby zapalić lampki lub „start the show”, aby uruchomić automatyczny pokaz bez zmieniania koloru, to system sprawdza w Dynamo DB, jaki jest ostatnio ustawiony kolor. W ten sposób Alexa zapamiętuje, jaki jest ustawiony kolor lampek drzewka. Następnie, sama akcja i informacja dotycząca koloru są zapisywane w cieniu rzeczy w AWS IoT.
- Jeśli cień zostanie zaktualizowany, wiadomość w postaci komendy MQTT zostanie wystawiona na temat Delta odpowiedniej rzeczy. Arduino Yun subskrybuje ten temat (uwaga – nazwa rzeczy tworzonej przez kod jest równa nadchodzącemu identyfikatorowi skill-id, wszystkie kropki zostają jedynie zastąpione myślnikiem. To może pomóc, przy próbie przebudowania projektu).
- Arduino odpytuje temat Delta, więc odbiera w ten sposób polecenia wysyłane wiadomością MQTT w danym temacie. Komendy przesyłane są do Arduino w formacie JSON. Informacje są wyodrębniane, a szkic Arduino wykonuje akcję na diodach LED zgodnie z tym, co jest podane w wiadomości (nowy kolor, uruchomienie pokazu, włączanie i wyłączanie lampek).
- Na koniec Arduino wysyła wiadomość MQTT do tematu aktualizacji AWS IoT, aby poinformować system, że akcja została poprawnie wykonana.
- Wiadomość zwrotna jest odbierana przez usługę AWS IoT, a zawarte w niej informacje o stanie lampek są zapisywane z powrotem w cieniu rzeczy, jako stan raportowany. Możliwe jest odczytywanie ostatniego stanu lampek z cienia rzeczy, zamiast szukać go w bazie Dynamo DB, jednakże z uwagi na fakt, że MQTT jest asynchroniczny i nie można polegać na Arduino, że udzieli natychmiastowej odpowiedzi, stan zapisany w cieniu rzeczy nie zawsze jest aktualny.
- Ten krok dzieje się, zasadniczo, zaraz po kroku trzecim, ponieważ skrypt jest celowo oddzielony od zaplecza sprzętowego. Tak więc zaraz po zaktualizowaniu cienia rzeczy w AWS IoT skrypt zwraca wyjściowy tekst do modułu mowy i opcjonalnie tag SSML z zawartością audio. Pliki MP3, które są następnie odtwarzane przez asystenta Alexa są pobierane z AWS S3 bucketa.
- Finalnie asystent Alexa odczytuje tekst zwrócony przez skrypt i odtwarza dźwięk na polecenie głosowe.
Wskaźnik statusu
Podczas gdy Arduino wykonuje swoją pracę, informuje również o swoim aktualnym stanie na pierwszej diodzie RGB LED w łańcuchu diod. Sygnalizacja jest następująca:
- mrugająca raz czerwona dioda wskazuje na awarię konfiguracji połączenia AWS IoT,
- dwukrotne migające czerwone światło oznacza nieudaną próbę połączenia z AWS IoT,
- trzykrotne migające czerwone światło oznacza nieudaną konfigurację połączenia AWS IoT,
- zielona dioda wskazuje na udane połączenie z AWS IoT,
- niebieski kolor diody wskazuje na ciągłe odpytywanie tematu AWS IoT,
- żółta dioda oznacza z kolei błąd podczas odpytywania tematu AWS IoT.
Podczas uruchamiania można początkowo zobaczyć czerwone mrugające diody przez czas potrzebny na połączenie modułu Arduino z siecią Wi-Fi. Jeśli Wi-Fi jest połączone, zielona dioda, a następnie stale migające niebieskie światło wskazuje, że system jest gotowy na przyjmowanie polecenia.
Jeśli dioda informująca o stanie miga na żółty kolor, oznacza to, że nie można było dotrzeć do tematu AWS IoT. Jeśli tak się stanie (np. gdy Arduino utraci połączenie z siecią Wi-Fi), próbuje jeszcze dziewięć razy, aż automatycznie spróbuje ponownie się połączyć.
Oznacza to, że po dziesięciokrotnym mrugnięciu diody na żółto, powinno wystąpić czerwone mrugnięcie i finalnie zapalenie się na zielono po ponownym połączeniu. Gdy Arduino ponownie połączy się z Wi-Fi i komunikacja z AWS IoT zostanie ponownie nawiązana, pojawi się mruganie na niebiesko.
Firmware
Na listingu 1 pokazano zawartość szkicu Arduino, sterującego lampkami choinkowymi. Ze szkicu usunięto część funkcji dla poprawy czytelności kodu. Kompletny kod programu, wraz z plikiem nagłówkowym (w którym zdefiniowane są dane do połączenia się z AWS IoT) można pobrać z repozytorium projektu na Githubie.
Podczas konfiguracji Arduino inicjalizuje diody LED i zapala je na pomarańczowo, a następnie łączy się z kolejką MQTT, za co odpowiedzialna jest funkcja connect(). Podczas połączenia do urządzenia CLIENT_ID dodawana jest na końcu losowa cyfra. Zapobiega to powstawaniu konfliktów z poprzednim połączeniem, na przykład po resecie połączenia. W funkcji tej zawarto także część obsługi diody statusowej w przypadku awarii połączenia z AWS IoT.
Po połączeniu z AWS układ przechodzi do głównej pętli programu loop(), gdzie dioda statusowa zapalana jest na żółto. Tam także zaszyta jest logika zliczania do 10 przy niepowodzeniach w komunikacji, przed zresetowaniem połączenia z MQTT.
Główna obsługa systemu zaszyta jest w funkcji msg_callback_delta(char* src, unsigned int len, Message_status_t flag), która wywoływana jest przez bibliotekę obsługującą MQTT za każdym razem, gdy w systemie pojawi się wiadomość w temacie, jaki jest zasubskrybowany. Wiadomość zapisana jest w formacie JSON. Znaczna część tej funkcji odpowiedzialna jest za parsowanie tej funkcji. Przede wszystkim uzyskiwany jest tryb działania lampek, tj. komenda, jaką wydano głosowo. System obsługuje następujące komendy:
- COLOR – zmiana koloru lampek,
- ON/OFF – odpowiednio, włączenie i wyłączenie lampek,
- SHOW – zaprogramowany pokaz oświetlenia,
- STOP – zatrzymanie pokazu oświetlenia.
Oczywiście można dodać własne komendy i tryby, aby obsłużyć to, co zostanie zapisane w skrypcie, jaki pracuje w AWS Lambda.
Oprogramowanie AWS Lambda
Oprogramowanie pracujące po stronie chmury i jest rozbite jest wiele plików. Wszystkie one dostępne są w repozytorium na Githubie. Opisują one słowa kluczowe wykorzystywane do sterowania (w języku angielskim oraz niemieckim), wartości, na jakie słowa te mają się przekładać – w tym przypadku, tłumaczenie kolorów na wartości RGB. Zawarto tam też komunikaty słowne, którymi odpowiedzieć może Alexa na komunikaty od użytkownika. Każdy komunikat ma osobny handler obsługujący dany komunikat, który w konkretny sposób konfiguruje model urządzenia, utrzymywany w AWS. Dokładny opis oprogramowania w Javie, jaki pracuje po stronie serwera, wykracza stanowczo poza ramy tego artykułu jak i naszego miesięcznika.
Podsumowanie
Na rysunku 2 pokazano gotowy system w postaci asystenta głosowego Alexa i znajdującej się nieopodal choinki. Na stronie projektu (patrz link źródłowy pod artykułem) można znaleźć nawet film, pokazujący światełka w działaniu. Projekt ten jest doskonałym punktem wejścia do bardziej złożonych systemów Internetu Rzeczy, wykorzystujących usługi w chmurze, takiej jak Amazon Web Services. Wykorzystanie tych usług – istniejących i ogólnie dostępnych lub tworzenie własnych, jest doskonałym sposobem na rozbudowę funkcjonalności dowolnego urządzenia Internetu Rzeczy.
Wykorzystanie asystentów głosowych jest również doskonałym sposobem na tworzenie ciekawszych i wygodniejszych w obsłudze urządzeń. Dzięki zastosowaniu takiego rozwiązania, systemy mogą być sterowane niemalże bez angażowania użytkownika. Jest to rozwiązanie często stosowane np. w automatyce domowej, jednak mechanizm ten można przenieść do wielu innych dziedzin.
Nikodem Czechowski, EP
Źródło: